265 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
65 changed files with 3316 additions and 1178 deletions

637
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@ itertools = "0.13"
job_scheduler_ng = "2.0"
ureq = { version = "2.9", features = ["json"] }
regex = "1.10"
urlencoding = "2.1"
[target.'cfg(not(windows))'.dependencies]
openssl = { version = "0.10", features = [ "vendored" ] }

View File

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

2
fd
View File

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

View File

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

View File

@ -18,7 +18,7 @@ test("cox can create and delete trip", async ({ page }) => {
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
await page.goto("/planned");
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 expect(page.locator("body")).toContainText("Erfolgreich gelöscht!");
});
@ -52,11 +52,11 @@ test.describe("cox can edit trips", () => {
test("edit remarks", async () => {
await sharedPage.goto("/planned");
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").fill("Meine Anmerkung");
await sharedPage.getByRole("button", { name: "Speichern" }).click();
await sharedPage.getByRole("link", { name: "Details" }).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Meine Anmerkung",
);
@ -68,14 +68,14 @@ test.describe("cox can edit trips", () => {
test("add and remove guest", async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.locator("#sidebar #user_note").click();
await sharedPage.locator("#sidebar #user_note").fill("Mein Gast");
await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click();
await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich angemeldet!",
);
await sharedPage.getByRole("link", { name: "Details" }).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 4",
);
@ -90,7 +90,7 @@ test.describe("cox can edit trips", () => {
await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich abgemeldet!",
);
await sharedPage.getByRole("link", { name: "Details" }).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5",
);
@ -108,7 +108,7 @@ test.describe("cox can edit trips", () => {
test("change amount rower", async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5",
);
@ -122,7 +122,7 @@ test.describe("cox can edit trips", () => {
test("call off trip", async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 3",
);
@ -137,7 +137,7 @@ test.describe("cox can edit trips", () => {
test.afterAll(async () => {
await sharedPage.goto("/planned");
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.close();
});

View File

@ -102,6 +102,28 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
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');
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByText('(cox2)').click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
@ -189,4 +211,173 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
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');
//Ausloggen...
await page.context().clearCookies();
await page.goto("/auth");
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByText('(cox2)').click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});
test("Cox can start and finish trip with cox steering only", 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: "cox_only_steering_boat" }).click();
} else {
await page.getByText('2+', { exact: true }).click();
await page.getByText("cox_only_steering_boat", { exact: true }).click();
}
// 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(
"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("cox_only_steering_boat");
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('cox_only_steering_boat');
await expect(page.locator('body')).toContainText('(cox2 - handgesteuert)');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByText('(cox2 - handgesteuert)').click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});
test("Kiosk can start and finish trip in one stop", 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 page.getByLabel('Ankunftszeit').click();
await page.locator('#destination').fill('a');
await page.getByLabel('Distanz').fill('1');
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 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('a (1 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
//Ausloggen...
await page.context().clearCookies();
await page.goto("/auth");
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByText('(cox2)').click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});

View File

@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS "user" (
"phone" text,
"address" text,
"family_id" INTEGER REFERENCES family(id),
"membership_pdf" BLOB
"membership_pdf" BLOB,
"user_token" TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
);
CREATE TABLE IF NOT EXISTS "family" (
@ -213,3 +214,9 @@ CREATE TABLE IF NOT EXISTS "trailer_reservation" (
"created_at" datetime not null default CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "distance" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"destination" text NOT NULL,
"distance_in_km" integer NOT NULL
);

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

@ -9,6 +9,9 @@ INSERT INTO "role" (name) VALUES ('paid');
INSERT INTO "role" (name) VALUES ('Vorstand');
INSERT INTO "role" (name) VALUES ('Bootsführer');
INSERT INTO "role" (name) VALUES ('schnupperant');
INSERT INTO "role" (name) VALUES ('kassier');
INSERT INTO "role" (name) VALUES ('schriftfuehrer');
INSERT INTO "role" (name) VALUES ('no-einschreibgebuehr');
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
@ -35,12 +38,17 @@ INSERT INTO "user_role" (user_id, role_id) VALUES(8,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(8,7);
INSERT INTO "user" (name, pw) VALUES('Vorstandsmitglied', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(9,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(9,9);
INSERT INTO "user" (name, pw) VALUES('main', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(10,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,6);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,9);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event');
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'trip_details for a planned event');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, '1970-01-02', 'trip_details for trip from cox');
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, date('now', '+1 day'), 'trip_details for trip from cox');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2);
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '&#127941;');
@ -55,6 +63,7 @@ INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Kaputtes Boot :-('
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 "boat" (name, amount_seats, location_id, default_shipmaster_only_steering) VALUES ('cox_only_steering_boat', 3, 1, true);
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');
@ -66,3 +75,5 @@ INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat
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');
insert into distance(destination, distance_in_km) values('Ottensheim', 25);

View File

@ -10,6 +10,18 @@ pub mod rest;
pub mod scheduled;
pub(crate) const AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD: i64 = 10;
pub(crate) const RENNRUDERBEITRAG: i32 = 11000;
pub(crate) const BOAT_STORAGE: i32 = 4500;
pub(crate) const FAMILY_TWO: i32 = 30000;
pub(crate) const FAMILY_THREE_OR_MORE: i32 = 35000;
pub(crate) const STUDENT_OR_PUPIL: i32 = 8000;
pub(crate) const REGULAR: i32 = 22000;
pub(crate) const UNTERSTUETZEND: i32 = 2500;
pub(crate) const FOERDERND: i32 = 8500;
pub(crate) const SCHECKBUCH: i32 = 3000;
pub(crate) const EINSCHREIBGEBUEHR: i32 = 3000;
#[cfg(test)]
#[macro_export]
macro_rules! testdb {

View File

@ -1,5 +1,6 @@
use std::ops::DerefMut;
use chrono::NaiveDateTime;
use itertools::Itertools;
use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
@ -170,6 +171,16 @@ AND date('now') BETWEEN start_date AND end_date;",
.is_some()
}
pub(crate) fn cat(&self) -> String {
if self.external {
"Vereinsfremde Boote".to_string()
} else if self.default_shipmaster_only_steering {
format!("{}+", self.amount_seats - 1)
} else {
format!("{}x", self.amount_seats)
}
}
async fn boats_to_details(db: &SqlitePool, boats: Vec<Boat>) -> Vec<BoatWithDetails> {
let mut res = Vec::new();
for boat in boats {
@ -180,13 +191,7 @@ AND date('now') BETWEEN start_date AND end_date;",
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)
};
let cat = boat.cat();
res.push(BoatWithDetails {
damage,
@ -391,6 +396,39 @@ ORDER BY amount_seats DESC
.await
.ok()
}
pub async fn on_water_between(
&self,
db: &mut Transaction<'_, Sqlite>,
dep: NaiveDateTime,
arr: NaiveDateTime,
) -> bool {
let dep = dep.format("%Y-%m-%dT%H:%M").to_string();
let arr = arr.format("%Y-%m-%dT%H:%M").to_string();
sqlx::query!(
"SELECT COUNT(*) AS overlap_count
FROM logbook
WHERE boat_id = ?
AND (
(departure <= ? AND arrival >= ?) -- Existing entry covers the entire new period
OR (departure >= ? AND departure < ?) -- Existing entry starts during the new period
OR (arrival > ? AND arrival <= ?) -- Existing entry ends during the new period
);",
self.id,
arr,
arr,
dep,
dep,
dep,
arr
)
.fetch_one(db.deref_mut())
.await
.unwrap()
.overlap_count
> 0
}
}
#[cfg(test)]

33
src/model/distance.rs Normal file
View File

@ -0,0 +1,33 @@
use serde::Serialize;
use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Serialize, Clone, Debug)]
pub struct Distance {
pub id: i64,
pub destination: String,
pub distance_in_km: i64,
}
impl Distance {
/// Return all default `distance`s, ordered by usage in logbook entries
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"SELECT
d.id,
d.destination,
d.distance_in_km
FROM
distance d
LEFT JOIN
logbook l ON d.destination = l.destination AND d.distance_in_km = l.distance_in_km
GROUP BY
d.id, d.destination, d.distance_in_km
ORDER BY
COUNT(l.id) DESC, d.destination ASC;"
)
.fetch_all(db)
.await
.unwrap()
}
}

View File

@ -8,7 +8,10 @@ use ics::{
use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool};
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
use super::{
notification::Notification, role::Role, tripdetails::TripDetails, triptype::TripType,
user::User,
};
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
pub struct Event {
@ -180,6 +183,17 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
.unwrap() //TODO: fixme
}
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Event> {
let mut ret = Vec::new();
let events = Self::all(db).await;
for event in events {
if event.is_rower_registered(db, user).await {
ret.push(event);
}
}
ret
}
//TODO: add tests
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
let is_rower = sqlx::query!(
@ -213,12 +227,40 @@ WHERE trip_details.id=?
.ok()
}
async fn advertise(db: &SqlitePool, day: &str, planned_starting_time: &str, name: &str) {
let donau = Role::find_by_name(db, "Donau Linz").await.unwrap();
Notification::create_for_role(
db,
&donau,
&format!("Am {} um {} wurde ein neues Event angelegt: {} Wir freuen uns wenn du dabei mitmachst, die Anmeldung ist ab sofort offen :-)", day, planned_starting_time, name),
"Neues Event",
Some(&format!("/planned#{day}")),
None,
)
.await;
}
pub async fn create(
db: &SqlitePool,
name: &str,
planned_amount_cox: i32,
always_show: bool,
trip_details: &TripDetails,
) {
if trip_details.always_show {
Self::advertise(
db,
&trip_details.day,
&trip_details.planned_starting_time,
name,
)
.await;
}
if always_show && !trip_details.always_show {
trip_details.set_always_show(db, true).await;
}
sqlx::query!(
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
name,
@ -258,6 +300,16 @@ WHERE trip_details.id=?
.await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
if !tripdetails.always_show && update.always_show {
Self::advertise(
db,
&tripdetails.day,
&tripdetails.planned_starting_time,
update.name,
)
.await;
}
if update.max_people == 0 && !was_already_cancelled {
let coxes = Registration::all_cox(db, self.id).await;
for user in coxes {
@ -353,38 +405,41 @@ WHERE trip_details.id=?
let events = Event::all(db).await;
for event in events {
let mut vevent =
ics::Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
vevent.push(DtStart::new(format!(
"{}T{}00",
event.day.replace('-', ""),
event.planned_starting_time.replace(':', "")
)));
let tripdetails = event.trip_details(db).await;
let mut name = String::new();
if event.is_cancelled() {
name.push_str("ABGESAGT");
if let Some(notes) = &tripdetails.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
name.push_str(&format!("{} ", event.name));
if let Some(triptype) = tripdetails.triptype(db).await {
name.push_str(&format!("{} ", triptype.name))
}
vevent.push(Summary::new(name));
calendar.add_event(vevent);
calendar.add_event(event.get_vevent(db).await);
}
let mut buf = Vec::new();
write!(&mut buf, "{}", calendar).unwrap();
String::from_utf8(buf).unwrap()
}
pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event {
let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000");
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
self.planned_starting_time.replace(':', "")
)));
let tripdetails = self.trip_details(db).await;
let mut name = String::new();
if self.is_cancelled() {
name.push_str("ABGESAGT");
if let Some(notes) = &tripdetails.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
name.push_str(&format!("{} ", self.name));
if let Some(triptype) = tripdetails.triptype(db).await {
name.push_str(&format!("{} ", triptype.name))
}
vevent.push(Summary::new(name));
vevent
}
pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails {
TripDetails::find_by_id(db, self.trip_details_id)
.await
@ -397,14 +452,14 @@ mod test {
use crate::{model::tripdetails::TripDetails, testdb};
use super::Event;
use chrono::NaiveDate;
use chrono::Local;
use sqlx::SqlitePool;
#[sqlx::test]
fn test_get_day() {
let pool = testdb!();
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 1);
}
@ -414,9 +469,9 @@ mod test {
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
Event::create(&pool, "new-event".into(), 2, false, &trip_details).await;
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 2);
}
@ -427,7 +482,7 @@ mod test {
planned_event.delete(&pool).await.unwrap();
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 0);
}
@ -435,7 +490,8 @@ mod test {
fn test_ics() {
let pool = testdb!();
let today = Local::now().date_naive().format("%Y%m%d").to_string();
let actual = Event::get_ics_feed(&pool).await;
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
assert_eq!(format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"), actual);
}
}

View File

@ -75,7 +75,7 @@ GROUP BY family.id;"
}
pub async fn members(&self, db: &SqlitePool) -> Vec<User> {
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id)
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id = ?", self.id)
.fetch_all(db)
.await
.unwrap()

View File

@ -1,6 +1,6 @@
use std::ops::DerefMut;
use chrono::{Datelike, Local, NaiveDateTime};
use chrono::{Datelike, Duration, Local, NaiveDateTime};
use rocket::FromForm;
use serde::Serialize;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
@ -31,6 +31,12 @@ impl PartialEq for Logbook {
}
}
pub(crate) enum Filter {
SingleDayOnly,
MultiDazOnly,
None,
}
#[derive(FromForm, Debug, Clone)]
pub struct LogToAdd {
pub boat_id: i32,
@ -60,6 +66,22 @@ pub struct LogToFinalize {
pub rowers: Vec<i64>,
}
#[derive(FromForm, Debug, Clone)]
pub struct LogToUpdate {
pub id: i64,
pub boat_id: i64,
pub shipmaster: i64,
pub steering_person: i64,
pub shipmaster_only_steering: bool,
pub departure: String,
pub arrival: Option<String>,
pub destination: Option<String>,
pub distance_in_km: Option<i64>,
pub comments: Option<String>,
pub logtype: Option<i64>,
pub rowers: Vec<i64>,
}
impl TryFrom<LogToAdd> for LogToFinalize {
type Error = String;
@ -94,6 +116,34 @@ pub struct LogbookWithBoatAndRowers {
pub rowers: Vec<User>,
}
impl LogbookWithBoatAndRowers {
pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self {
let mut tx = db.begin().await.unwrap();
let ret = Self::from_tx(&mut tx, log).await;
tx.commit().await.unwrap();
ret
}
pub(crate) async fn from_tx(db: &mut Transaction<'_, Sqlite>, log: Logbook) -> Self {
Self {
rowers: Rower::for_log_tx(db, &log).await,
boat: Boat::find_by_id_tx(db, log.boat_id as i32).await.unwrap(),
shipmaster_user: User::find_by_id_tx(db, log.shipmaster as i32)
.await
.unwrap(),
steering_user: User::find_by_id_tx(db, log.steering_person as i32)
.await
.unwrap(),
logbook: log,
}
}
}
#[derive(Debug, PartialEq)]
pub enum LogbookAdminUpdateError {
NotAllowed,
}
#[derive(Debug, PartialEq)]
pub enum LogbookUpdateError {
NotYourEntry,
@ -105,6 +155,9 @@ pub enum LogbookUpdateError {
UserNotAllowedToUseBoat,
OnlyAllowedToEndTripsEndingToday,
TooFast(i64, i64),
AlreadyFinalized,
ExternalSteeringPersonMustSteerOrShipmaster,
BoatAlreadyOnWater,
}
#[derive(Debug, PartialEq)]
@ -129,6 +182,8 @@ pub enum LogbookCreateError {
OnlyAllowedToEndTripsEndingToday,
CantChangeHandoperatableStatusForThisBoat,
TooFast(i64, i64),
AlreadyFinalized,
ExternalSteeringPersonMustSteerOrShipmaster,
}
impl From<LogbookUpdateError> for LogbookCreateError {
@ -153,6 +208,11 @@ impl From<LogbookUpdateError> for LogbookCreateError {
LogbookCreateError::OnlyAllowedToEndTripsEndingToday
}
LogbookUpdateError::TooFast(km, min) => LogbookCreateError::TooFast(km, min),
LogbookUpdateError::AlreadyFinalized => LogbookCreateError::AlreadyFinalized,
LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster => {
LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster
}
LogbookUpdateError::BoatAlreadyOnWater => LogbookCreateError::BoatAlreadyOnWater,
}
}
}
@ -172,7 +232,7 @@ impl Logbook {
.await
.ok()
}
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"
@ -219,15 +279,7 @@ ORDER BY departure DESC
let mut ret = Vec::new();
for log in logs {
ret.push(LogbookWithBoatAndRowers {
rowers: Rower::for_log(db, &log).await,
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
steering_user: User::find_by_id(db, log.steering_person as i32)
.await
.unwrap(),
logbook: log,
});
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
}
ret
}
@ -235,6 +287,16 @@ ORDER BY departure DESC
pub async fn completed_with_user(
db: &SqlitePool,
user: &User,
) -> Vec<LogbookWithBoatAndRowers> {
let mut tx = db.begin().await.unwrap();
let ret = Self::completed_with_user_tx(&mut tx, user).await;
tx.commit().await.unwrap();
ret
}
pub async fn completed_with_user_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> Vec<LogbookWithBoatAndRowers> {
let logs = sqlx::query_as(
&format!("
@ -242,24 +304,103 @@ ORDER BY departure DESC
FROM logbook
JOIN rower ON logbook.id = rower.logbook_id
WHERE arrival is not null AND rower_id = {}
ORDER BY departure DESC
ORDER BY arrival DESC
", user.id)
)
.fetch_all(db.deref_mut())
.await
.unwrap(); //TODO: fixme
let mut ret = Vec::new();
for log in logs {
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
}
ret
}
pub async fn year_first_logbook_entry(db: &SqlitePool, user: &User) -> Option<i32> {
let log: Option<Self> = sqlx::query_as(
&format!("
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
FROM logbook
JOIN rower ON logbook.id = rower.logbook_id
WHERE arrival is not null AND rower_id = {}
ORDER BY arrival
LIMIT 1
", user.id)
)
.fetch_optional(db)
.await
.unwrap(); //TODO: fixme
if let Some(log) = log {
Some(log.arrival.unwrap().year())
} else {
None
}
}
pub async fn year_last_logbook_entry(db: &SqlitePool, user: &User) -> Option<i32> {
let log: Option<Self> = sqlx::query_as(
&format!("
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
FROM logbook
JOIN rower ON logbook.id = rower.logbook_id
WHERE arrival is not null AND rower_id = {}
ORDER BY arrival DESC
LIMIT 1
", user.id)
)
.fetch_optional(db)
.await
.unwrap(); //TODO: fixme
if let Some(log) = log {
Some(log.arrival.unwrap().year())
} else {
None
}
}
pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year(
db: &SqlitePool,
user: &User,
min_distance: i32,
year: i32,
filter: Filter,
) -> Vec<LogbookWithBoatAndRowers> {
let logs: Vec<Logbook> = sqlx::query_as(
&format!("
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
FROM logbook
JOIN rower ON logbook.id = rower.logbook_id
WHERE arrival is not null AND rower_id = {} AND logtype = 1 AND distance_in_km >= {} AND arrival like '{}-%'
ORDER BY arrival DESC
", user.id, min_distance, year)
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
let mut ret = Vec::new();
for log in logs {
ret.push(LogbookWithBoatAndRowers {
rowers: Rower::for_log(db, &log).await,
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
steering_user: User::find_by_id(db, log.steering_person as i32)
.await
.unwrap(),
logbook: log,
});
let trip_days = log.arrival.unwrap() - log.departure;
let trip_days = trip_days.num_days();
match filter {
Filter::SingleDayOnly => {
if trip_days == 0 {
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
}
}
Filter::MultiDazOnly => {
if trip_days > 0 {
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
}
}
Filter::None => {
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
}
}
}
ret
}
@ -275,7 +416,7 @@ ORDER BY departure DESC
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
FROM logbook
WHERE arrival is not null AND arrival LIKE '{}-%'
ORDER BY departure DESC
ORDER BY arrival DESC
", year)
)
.fetch_all(db)
@ -284,15 +425,7 @@ ORDER BY departure DESC
let mut ret = Vec::new();
for log in logs {
ret.push(LogbookWithBoatAndRowers {
rowers: Rower::for_log(db, &log).await,
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
steering_user: User::find_by_id(db, log.steering_person as i32)
.await
.unwrap(),
logbook: log,
});
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
}
ret
}
@ -301,6 +434,7 @@ ORDER BY departure DESC
db: &SqlitePool,
mut log: LogToAdd,
created_by_user: &User,
smtp_pw: &str,
) -> Result<String, LogbookCreateError> {
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
return Err(LogbookCreateError::BoatNotFound);
@ -329,13 +463,12 @@ ORDER BY departure DESC
let mut tx = db.begin().await.unwrap();
let inserted_row = sqlx::query!(
"INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING id",
"INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?) RETURNING id",
log.boat_id,
log.shipmaster,
log.steering_person,
log.shipmaster_only_steering,
log.departure,
log.arrival,
log.destination,
log.distance_in_km,
log.comments,
@ -349,7 +482,7 @@ ORDER BY departure DESC
.unwrap(); //ok
return match logbook
.home_with_transaction(&mut tx, created_by_user, log_to_finalize)
.home_with_transaction(&mut tx, created_by_user, log_to_finalize, smtp_pw)
.await
{
Ok(_) => {
@ -391,6 +524,18 @@ ORDER BY departure DESC
if user.on_water(db).await {
return Err(LogbookCreateError::RowerAlreadyOnWater(Box::new(user)));
}
if user.name == "Externe Steuerperson" {
if let (Some(steering_id), Some(shipmaster_id)) =
(log.steering_person, log.shipmaster)
{
if steering_id != user.id && shipmaster_id != user.id {
return Err(
LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster,
);
}
}
}
}
if !boat.shipmaster_allowed(db, created_by_user).await {
@ -437,23 +582,33 @@ ORDER BY departure DESC
Ok(ret)
}
pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> {
let result = sqlx::query!("SELECT destination, distance_in_km FROM logbook WHERE id IN (SELECT MIN(id) FROM logbook GROUP BY destination) AND destination IS NOT NULL AND distance_in_km IS NOT NULL;")
.fetch_all(db)
.await
.unwrap();
pub async fn update(
&self,
db: &SqlitePool,
data: LogToUpdate,
user: &User,
) -> Result<(), LogbookAdminUpdateError> {
if !user.has_role(db, "Vorstand").await {
return Err(LogbookAdminUpdateError::NotAllowed);
}
result
.into_iter()
.filter_map(|r| {
if let (Some(destination), Some(distance_in_km)) = (r.destination, r.distance_in_km)
{
Some((destination, distance_in_km))
} else {
None
}
})
.collect()
sqlx::query!(
"UPDATE logbook SET boat_id=?, shipmaster=?, steering_person=?, shipmaster_only_steering=?, departure=?, arrival=?, destination=?, distance_in_km=?, comments=?, logtype=? WHERE id=?",
data.boat_id,
data.shipmaster,
data.steering_person,
data.shipmaster_only_steering,
data.departure,
data.arrival,
data.destination,
data.distance_in_km,
data.comments,
data.logtype,
self.id
)
.execute(db)
.await.unwrap();
Ok(())
}
async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) {
@ -478,9 +633,11 @@ ORDER BY departure DESC
db: &SqlitePool,
user: &User,
log: LogToFinalize,
smtp_pw: &str,
) -> Result<(), LogbookUpdateError> {
let mut tx = db.begin().await.unwrap();
self.home_with_transaction(&mut tx, user, log).await?;
self.home_with_transaction(&mut tx, user, log, smtp_pw)
.await?;
tx.commit().await.unwrap();
Ok(())
}
@ -490,12 +647,17 @@ ORDER BY departure DESC
db: &mut Transaction<'_, Sqlite>,
user: &User,
mut log: LogToFinalize,
smtp_pw: &str,
) -> Result<(), LogbookUpdateError> {
//TODO: extract common tests with `create()`
if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster {
return Err(LogbookUpdateError::NotYourEntry);
}
if self.arrival.is_some() {
return Err(LogbookUpdateError::AlreadyFinalized);
}
let boat = Boat::find_by_id_tx(db, self.boat_id as i32).await.unwrap(); //ok
if boat.amount_seats == 1 {
@ -529,6 +691,10 @@ ORDER BY departure DESC
return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
}
if !boat.external && boat.on_water_between(db, dep, arr).await {
return Err(LogbookUpdateError::BoatAlreadyOnWater);
}
let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60;
// Not possible to row < 1 min / 500 m = < 2 min / km
let possible_distance_km = duration_in_mins / 2;
@ -542,7 +708,12 @@ ORDER BY departure DESC
let today = Local::now().date_naive();
let day_diff = today - arr.date();
let day_diff = day_diff.num_days();
if day_diff >= 7 && !user.has_role_tx(db, "admin").await {
if day_diff >= 7
&& !user.has_role_tx(db, "admin").await
&& !user
.has_role_tx(db, "allow-retroactive-logbookentries")
.await
{
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
}
if day_diff < 0 && !user.has_role_tx(db, "admin").await {
@ -553,6 +724,19 @@ ORDER BY departure DESC
self.remove_rowers(db).await;
for rower in &log.rowers {
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
if user.name == "Externe Steuerperson" {
if let (Some(steering_id), Some(shipmaster_id)) =
(log.steering_person, log.shipmaster)
{
if steering_id != user.id && shipmaster_id != user.id {
return Err(
LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster,
);
}
}
}
Rower::create(db, self.id, *rower)
.await
.map_err(|e| LogbookUpdateError::RowerCreateError(*rower, e.to_string()))?;
@ -617,21 +801,68 @@ ORDER BY departure DESC
).await;
}
for rower in &log.rowers {
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
user.received_new_logentry(db, smtp_pw).await;
}
Ok(())
}
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await;
if user.has_role(db, "admin").await
|| user.has_role(db, "Vorstand").await
|| user.id == self.shipmaster
{
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Logbook of a valid id
return Ok(());
if self.arrival.is_none() {
if user.has_role(db, "admin").await
|| user.has_role(db, "Vorstand").await
|| user.id == self.shipmaster
{
let now = Local::now().naive_local();
let difference = now - self.departure;
if difference > Duration::hours(1) {
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await;
let mut msg = format!("{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}", user.name, logbook.steering_user.name, logbook.steering_user.name, logbook.logbook.departure.format("%Y-%m-%d %H:%M"));
if let Some(destination) = logbook.logbook.destination {
msg.push_str(&format!(", Ziel: {}", destination));
} else {
msg.push_str(", kein Ziel eingegeben");
}
msg.push_str(", Ruderer: ");
let mut it = logbook.rowers.clone().into_iter().peekable();
while let Some(rower) = it.next() {
msg.push_str(&rower.name);
if it.peek().is_some() {
msg.push_str(" + ");
}
}
Notification::create_for_role(
db,
&vorstand,
&msg,
"Ungewöhnliches Verhalten",
None,
None,
)
.await;
}
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Logbook of a valid id
return Ok(());
}
} else {
// Only admins can delete completed logbook entries
if user.has_role(db, "admin").await {
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Logbook of a valid id
return Ok(());
}
}
Err(LogbookDeleteError::NotYourEntry)
}
@ -711,6 +942,7 @@ mod test {
rowers: vec![4],
},
&User::find_by_id(&pool, 4).await.unwrap(),
"",
)
.await
.unwrap();
@ -742,6 +974,7 @@ mod test {
departure: format!("{}T10:00", start_date),
arrival: format!("{}T12:00", current_date),
},
"",
)
.await
.unwrap();
@ -762,6 +995,7 @@ mod test {
rowers: vec![2],
},
&User::find_by_id(&pool, 1).await.unwrap(),
"",
)
.await
.unwrap();
@ -791,6 +1025,7 @@ mod test {
rowers: vec![5],
},
&User::find_by_id(&pool, 4).await.unwrap(),
"",
)
.await;
@ -817,6 +1052,7 @@ mod test {
rowers: vec![5],
},
&User::find_by_id(&pool, 4).await.unwrap(),
"",
)
.await;
@ -843,6 +1079,7 @@ mod test {
rowers: vec![5],
},
&User::find_by_id(&pool, 5).await.unwrap(),
"",
)
.await;
@ -869,6 +1106,7 @@ mod test {
rowers: vec![5],
},
&User::find_by_id(&pool, 5).await.unwrap(),
"",
)
.await;
@ -895,6 +1133,7 @@ mod test {
rowers: Vec::new(),
},
&User::find_by_id(&pool, 2).await.unwrap(),
"",
)
.await;
@ -921,6 +1160,7 @@ mod test {
rowers: vec![5],
},
&User::find_by_id(&pool, 5).await.unwrap(),
"",
)
.await;
@ -947,27 +1187,13 @@ mod test {
rowers: vec![1, 5],
},
&User::find_by_id(&pool, 5).await.unwrap(),
"",
)
.await;
assert_eq!(res, Err(LogbookCreateError::TooManyRowers(1, 2)));
}
#[sqlx::test]
fn test_distances() {
let pool = testdb!();
let res = Logbook::distances(&pool).await;
assert_eq!(
res,
vec![
("Ottensheim".into(), 25 as i64),
("Ottensheim + Regattastrecke".into(), 29 as i64),
]
);
}
#[sqlx::test]
fn test_succ_home() {
let pool = testdb!();
@ -993,6 +1219,7 @@ mod test {
departure: format!("{}T10:00", current_date),
arrival: format!("{}T12:00", current_date),
},
"",
)
.await
.unwrap();
@ -1021,6 +1248,7 @@ mod test {
departure: "1990-01-01T10:00".into(),
arrival: "1990-01-01T12:00".into(),
},
"",
)
.await;
@ -1050,6 +1278,7 @@ mod test {
departure: "1990-01-01T10:00".into(),
arrival: "1990-01-01T12:00".into(),
},
"",
)
.await;

View File

@ -5,7 +5,7 @@ use lettre::{
transport::smtp::authentication::Credentials,
Message, SmtpTransport, Transport,
};
use sqlx::SqlitePool;
use sqlx::{Sqlite, SqlitePool, Transaction};
use crate::tera::admin::mail::MailToSend;
@ -20,6 +20,19 @@ impl Mail {
subject: &str,
body: String,
smtp_pw: &str,
) -> Result<(), String> {
let mut tx = db.begin().await.unwrap();
let ret = Self::send_single_tx(&mut tx, to, subject, body, smtp_pw).await;
tx.commit().await.unwrap();
ret
}
pub async fn send_single_tx(
db: &mut Transaction<'_, Sqlite>,
to: &str,
subject: &str,
body: String,
smtp_pw: &str,
) -> Result<(), String> {
let mut email = Message::builder()
.from(
@ -40,7 +53,7 @@ impl Mail {
match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => {
Log::create(
Log::create_with_tx(
db,
format!("Mail not sent to {single_rec}, because it could not be parsed"),
)
@ -182,7 +195,7 @@ dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
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\
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256. 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\
@ -293,7 +306,7 @@ Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
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.)
Bankverbindung: IBAN: AT58 2032 0321 0072 9256 (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");

View File

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

View File

@ -5,7 +5,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{role::Role, user::User};
use super::{role::Role, user::User, usertrip::UserTrip};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct Notification {
@ -140,15 +140,13 @@ ORDER BY read_at DESC, created_at DESC;
let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap();
if let Some(caps) = re.captures(action) {
if let Some(matched) = caps.get(1) {
if let Ok(number) = matched.as_str().parse::<i32>() {
let _ = sqlx::query!(
"DELETE FROM user_trip WHERE user_id = ? AND trip_details_id = ?",
self.user_id,
number
)
.execute(db)
.await
.unwrap();
if let Ok(number) = matched.as_str().parse::<i64>() {
if let Some(usertrip) =
UserTrip::find_by_userid_and_trip_detail_id(db, self.user_id, number)
.await
{
let _ = usertrip.self_delete(db).await;
}
}
}
}
@ -179,6 +177,14 @@ ORDER BY read_at DESC, created_at DESC;
.await
.unwrap();
}
pub(crate) async fn delete_by_link(db: &sqlx::Pool<Sqlite>, link: &str) {
let link = Some(link);
sqlx::query!("DELETE FROM notification WHERE link=?", link)
.execute(db)
.await
.unwrap();
}
}
#[cfg(test)]
@ -195,6 +201,7 @@ mod test {
testdb,
};
use chrono::Local;
use sqlx::SqlitePool;
#[sqlx::test]
@ -205,17 +212,16 @@ mod test {
let add_tripdetails = TripDetailsToAdd {
planned_starting_time: "10:00",
max_people: 4,
day: "1970-02-01".into(),
day: Local::now().date_naive().format("%Y-%m-%d").to_string(),
notes: None,
trip_type: None,
allow_guests: false,
always_show: false,
};
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
.await
.unwrap();
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
Event::create(&pool, "new-event".into(), 2, false, &trip_details).await;
let event = Event::find_by_trip_details(&pool, trip_details.id)
.await
.unwrap();

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

@ -0,0 +1,27 @@
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",
"Donau Linz - 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,87 @@
use serde::Serialize;
#[derive(Serialize, PartialEq, Debug)]
pub(crate) enum Level {
NONE,
BRONZE,
SILVER,
GOLD,
DIAMOND,
DONE,
}
impl Level {
fn required_km(&self) -> i32 {
match self {
Level::BRONZE => 40000,
Level::SILVER => 80000,
Level::GOLD => 100000,
Level::DIAMOND => 200000,
Level::DONE => 0,
Level::NONE => 0,
}
}
fn next_level(km: i32) -> Self {
if km < Level::BRONZE.required_km() {
Level::BRONZE
} else if km < Level::SILVER.required_km() {
Level::SILVER
} else if km < Level::GOLD.required_km() {
Level::GOLD
} else if km < Level::DIAMOND.required_km() {
Level::DIAMOND
} else {
Level::DONE
}
}
pub(crate) fn curr_level(km: i32) -> Self {
if km < Level::BRONZE.required_km() {
Level::NONE
} else if km < Level::SILVER.required_km() {
Level::BRONZE
} else if km < Level::GOLD.required_km() {
Level::SILVER
} else if km < Level::DIAMOND.required_km() {
Level::GOLD
} else {
Level::DIAMOND
}
}
pub(crate) fn desc(&self) -> &str {
match self {
Level::BRONZE => "Bronze",
Level::SILVER => "Silber",
Level::GOLD => "Gold",
Level::DIAMOND => "Diamant",
Level::DONE => "",
Level::NONE => "-",
}
}
}
#[derive(Serialize)]
pub(crate) struct Next {
level: Level,
desc: String,
missing_km: i32,
required_km: i32,
rowed_km: i32,
}
impl Next {
pub(crate) fn new(rowed_km: i32) -> Self {
let level = Level::next_level(rowed_km);
let required_km = level.required_km();
let missing_km = required_km - rowed_km;
Self {
desc: level.desc().to_string(),
level,
missing_km,
required_km,
rowed_km,
}
}
}

52
src/model/personal/mod.rs Normal file
View File

@ -0,0 +1,52 @@
use chrono::{Datelike, Local};
use equatorprice::Level;
use serde::Serialize;
use sqlx::SqlitePool;
use super::{logbook::Logbook, stat::Stat, user::User};
pub(crate) mod cal;
pub(crate) mod equatorprice;
pub(crate) mod rowingbadge;
#[derive(Serialize)]
pub(crate) struct Achievements {
pub(crate) equatorprice: equatorprice::Next,
pub(crate) curr_equatorprice_name: String,
pub(crate) new_equatorprice_this_season: bool,
pub(crate) rowingbadge: Option<rowingbadge::Status>,
pub(crate) all_time_km: i32,
pub(crate) year_first_mentioned: Option<i32>,
pub(crate) year_last_mentioned: Option<i32>,
}
impl Achievements {
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Self {
let rowed_km = Stat::total_km(db, user).await.rowed_km;
let rowed_km_this_season = if Local::now().month() == 1 {
Stat::person(db, Some(Local::now().year() - 1), user)
.await
.rowed_km
+ Stat::person(db, Some(Local::now().year()), user)
.await
.rowed_km
} else {
Stat::person(db, Some(Local::now().year()), user)
.await
.rowed_km
};
let new_equatorprice_this_season =
Level::curr_level(rowed_km) != Level::curr_level(rowed_km - rowed_km_this_season);
Self {
equatorprice: equatorprice::Next::new(rowed_km),
curr_equatorprice_name: equatorprice::Level::curr_level(rowed_km).desc().to_string(),
new_equatorprice_this_season,
rowingbadge: rowingbadge::Status::for_user(db, user).await,
all_time_km: rowed_km,
year_first_mentioned: Logbook::year_first_logbook_entry(db, user).await,
year_last_mentioned: Logbook::year_last_logbook_entry(db, user).await,
}
}
}

View File

@ -0,0 +1,166 @@
use std::cmp;
use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize;
use sqlx::SqlitePool;
use crate::model::{
logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
stat::Stat,
user::User,
};
enum AgeBracket {
Till14,
From14Till18,
From19Till30,
From31Till60,
From61Till75,
From76,
}
impl AgeBracket {
fn cat(&self) -> &str {
match self {
AgeBracket::Till14 => "Schülerinnen und Schüler bis 14 Jahre",
AgeBracket::From14Till18 => "Juniorinnen und Junioren, Para-Ruderer bis 18 Jahre",
AgeBracket::From19Till30 => "Frauen und Männer, Para-Ruderer bis 30 Jahre",
AgeBracket::From31Till60 => "Frauen und Männer, Para-Ruderer von 31 bis 60 Jahre",
AgeBracket::From61Till75 => "Frauen und Männer, Para-Ruderer von 61 bis 75 Jahre",
AgeBracket::From76 => "Frauen und Männer, Para-Ruderer ab 76 Jahre",
}
}
fn dist_in_km(&self) -> i32 {
match self {
AgeBracket::Till14 => 500,
AgeBracket::From14Till18 => 1000,
AgeBracket::From19Till30 => 1200,
AgeBracket::From31Till60 => 1000,
AgeBracket::From61Till75 => 800,
AgeBracket::From76 => 600,
}
}
fn required_dist_multi_day_in_km(&self) -> i32 {
match self {
AgeBracket::Till14 => 60,
AgeBracket::From14Till18 => 60,
AgeBracket::From19Till30 => 80,
AgeBracket::From31Till60 => 80,
AgeBracket::From61Till75 => 80,
AgeBracket::From76 => 80,
}
}
fn required_dist_single_day_in_km(&self) -> i32 {
match self {
AgeBracket::Till14 => 30,
AgeBracket::From14Till18 => 30,
AgeBracket::From19Till30 => 40,
AgeBracket::From31Till60 => 40,
AgeBracket::From61Till75 => 40,
AgeBracket::From76 => 40,
}
}
}
impl TryFrom<&User> for AgeBracket {
type Error = String;
fn try_from(value: &User) -> Result<Self, Self::Error> {
let Some(birthdate) = value.birthdate.clone() else {
return Err("User has no birthdate".to_string());
};
let Ok(birthdate) = NaiveDate::parse_from_str(&birthdate, "%Y-%m-%d") else {
return Err("Birthdate in wrong format...".to_string());
};
let today = Local::now().date_naive();
let age = today.year() - birthdate.year();
if age <= 14 {
Ok(AgeBracket::Till14)
} else if age <= 18 {
Ok(AgeBracket::From14Till18)
} else if age <= 30 {
Ok(AgeBracket::From19Till30)
} else if age <= 60 {
Ok(AgeBracket::From31Till60)
} else if age <= 75 {
Ok(AgeBracket::From61Till75)
} else {
Ok(AgeBracket::From76)
}
}
}
#[derive(Serialize)]
pub(crate) struct Status {
pub(crate) year: i32,
rowed_km: i32,
category: String,
required_km: i32,
missing_km: i32,
multi_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
multi_day_trips_required_distance: i32,
single_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
single_day_trips_required_distance: i32,
achieved: bool,
}
impl Status {
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
let Ok(agebracket) = AgeBracket::try_from(user) else {
return None;
};
let category = agebracket.cat().to_string();
let year = if Local::now().month() == 1 {
Local::now().year() - 1
} else {
Local::now().year()
};
let rowed_km = Stat::person(db, Some(year), user).await.rowed_km;
let required_km = agebracket.dist_in_km();
let missing_km = cmp::max(required_km - rowed_km, 0);
let single_day_trips_over_required_distance =
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
db,
user,
agebracket.required_dist_single_day_in_km(),
year,
Filter::SingleDayOnly,
)
.await;
let multi_day_trips_over_required_distance =
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
db,
user,
agebracket.required_dist_multi_day_in_km(),
year,
Filter::MultiDazOnly,
)
.await;
let achieved = missing_km == 0
&& (multi_day_trips_over_required_distance.len() >= 1
|| single_day_trips_over_required_distance.len() >= 2);
Some(Self {
year,
rowed_km,
category,
required_km,
missing_km,
multi_day_trips_over_required_distance,
single_day_trips_over_required_distance,
multi_day_trips_required_distance: agebracket.required_dist_multi_day_in_km(),
single_day_trips_required_distance: agebracket.required_dist_single_day_in_km(),
achieved,
})
}
}

View File

@ -13,16 +13,23 @@ pub struct Rower {
impl Rower {
pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec<User> {
let mut tx = db.begin().await.unwrap();
let ret = Self::for_log_tx(&mut tx, log).await;
tx.commit().await.unwrap();
ret
}
pub async fn for_log_tx(db: &mut Transaction<'_, Sqlite>, 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
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
FROM user
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
",
log.id
)
.fetch_all(db)
.fetch_all(db.deref_mut())
.await
.unwrap()
}

View File

@ -16,6 +16,7 @@ pub struct BoatStat {
#[derive(Serialize, Clone)]
pub struct SingleBoatStat {
name: String,
cat: String,
location: String,
owner: String,
years: HashMap<String, i32>,
@ -71,6 +72,7 @@ ORDER BY
}
let year: String = format!("{year}");
let cat = boat.cat();
let rowed_km: i32 = row.get("rowed_km");
@ -80,6 +82,7 @@ ORDER BY
name,
location,
owner,
cat,
years: HashMap::new(),
});
boat_stat.years.insert(year, rowed_km);
@ -195,6 +198,32 @@ ORDER BY rowed_km DESC, u.name;
})
.collect()
}
pub async fn total_km(db: &SqlitePool, user: &User) -> Stat {
//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;
",
user.id
))
.fetch_one(db)
.await
.unwrap();
Stat {
name: row.get("name"),
rowed_km: row.get("rowed_km"),
}
}
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
let year = match year {
Some(year) => year,

View File

@ -1,13 +1,16 @@
use chrono::NaiveDate;
use chrono::{Local, NaiveDate};
use ics::properties::{DtStart, Summary};
use serde::Serialize;
use sqlx::SqlitePool;
use super::{
event::{Event, Registration},
log::Log,
notification::Notification,
tripdetails::TripDetails,
triptype::TripType,
user::{CoxUser, User},
usertrip::UserTrip,
};
#[derive(Serialize, Clone, Debug)]
@ -40,7 +43,6 @@ pub struct TripUpdate<'a> {
pub max_people: i32,
pub notes: Option<&'a str>,
pub trip_type: Option<i64>, //TODO: Move to `TripType`
pub always_show: bool,
pub is_locked: bool,
}
@ -77,23 +79,30 @@ impl Trip {
.await;
if same_starting_datetime.len() > 1 {
for notify in same_starting_datetime {
if notify.id != trip_details.id {
// notify everyone except oneself
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create(
db,
&user,
&format!(
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
cox.user.name, trip.day, trip.planned_starting_time
),
"Neue Ausfahrt zur selben Zeit",
None,
None,
)
.await;
}
// don't notify oneself
if notify.id == trip_details.id {
continue;
}
// don't notify people who have cancelled their trip
if notify.cancelled() {
continue;
}
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create(
db,
&user,
&format!(
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
cox.user.name, trip.day, trip.planned_starting_time
),
"Neue Ausfahrt zur selben Zeit",
None,
None,
)
.await;
}
}
}
@ -116,6 +125,68 @@ WHERE trip_details.id=?
.ok()
}
pub(crate) async fn get_vevent(self, user: &User) -> ics::Event {
let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000");
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
self.planned_starting_time.replace(':', "")
)));
let mut name = String::new();
if self.is_cancelled() {
name.push_str("ABGESAGT");
if let Some(notes) = &self.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
if self.cox_id == user.id {
name.push_str("Ruderausfahrt (selber ausgeschrieben)");
} else {
name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name));
}
vevent.push(Summary::new(name));
vevent
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
FROM trip
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
INNER JOIN user ON trip.cox_id = user.id
",
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
}
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Self> {
let mut ret = Vec::new();
let trips = Self::all(db).await;
for trip in trips {
if user.id == trip.cox_id {
ret.push(trip.clone());
}
if let Some(trip_details_id) = trip.trip_details_id {
if UserTrip::find_by_userid_and_trip_detail_id(db, user.id, trip_details_id)
.await
.is_some()
{
ret.push(trip);
}
}
}
ret
}
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
@ -164,6 +235,11 @@ WHERE trip.id=?
}
}
pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithUserAndType> {
let today = Local::now().date_naive();
Self::get_for_day(db, today).await
}
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUserAndType> {
let day = format!("{day}");
let trips = sqlx::query_as!(
@ -204,13 +280,18 @@ WHERE day=?
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
let was_already_cancelled = tripdetails.max_people == 0;
let is_locked = if update.max_people == 0 {
false
} else {
update.is_locked
};
sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, is_locked = ? WHERE id = ?",
update.max_people,
update.notes,
update.trip_type,
update.always_show,
update.is_locked,
is_locked,
trip_details_id
)
.execute(db)
@ -232,7 +313,7 @@ WHERE day=?
db,
&user,
&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.trip.day,
update.trip.planned_starting_time,
@ -313,18 +394,16 @@ WHERE day=?
return Err(TripDeleteError::SomebodyAlreadyRegistered);
}
if !self.is_trip_from_user(user.id) {
if !self.is_trip_from_user(user.id) && !user.has_role(db, "admin").await {
return Err(TripDeleteError::NotYourTrip);
}
sqlx::query!(
"DELETE FROM trip WHERE cox_id = ? AND id = ?",
user.id,
self.id
)
.execute(db)
.await
.unwrap(); //TODO: fixme
Log::create(db, format!("{} deleted trip: {:#?}", user.user.name, self)).await;
sqlx::query!("DELETE FROM trip WHERE id = ?", self.id)
.execute(db)
.await
.unwrap(); //TODO: fixme
Ok(())
}
@ -333,6 +412,20 @@ WHERE day=?
self.cox_id == user_id
}
pub(crate) async fn toggle_always_show(&self, db: &SqlitePool) {
if let Some(trip_details) = self.trip_details_id {
let new_state = !self.always_show;
sqlx::query!(
"UPDATE trip_details SET always_show = ? WHERE id = ?",
new_state,
trip_details
)
.execute(db)
.await
.unwrap();
}
}
pub(crate) async fn get_pinned_for_day(
db: &sqlx::Pool<sqlx::Sqlite>,
day: NaiveDate,
@ -341,6 +434,10 @@ WHERE day=?
trips.retain(|e| e.trip.always_show);
trips
}
fn is_cancelled(&self) -> bool {
self.max_people == 0
}
}
#[derive(Debug)]
@ -382,7 +479,7 @@ mod test {
testdb,
};
use chrono::NaiveDate;
use chrono::Local;
use sqlx::SqlitePool;
use super::Trip;
@ -409,7 +506,8 @@ mod test {
fn test_get_day_cox_trip() {
let pool = testdb!();
let res = Trip::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 2).unwrap()).await;
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
let res = Trip::get_for_day(&pool, tomorrow).await;
assert_eq!(res.len(), 1);
}
@ -465,7 +563,6 @@ mod test {
max_people: 10,
notes: None,
trip_type: None,
always_show: false,
is_locked: false,
};
@ -494,7 +591,6 @@ mod test {
max_people: 10,
notes: None,
trip_type: Some(1),
always_show: false,
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_ok());
@ -523,7 +619,6 @@ mod test {
max_people: 10,
notes: None,
trip_type: None,
always_show: false,
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_err());

View File

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

View File

@ -15,20 +15,14 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{
family::Family, log::Log, mail::Mail, notification::Notification, role::Role, stat::Stat,
tripdetails::TripDetails, Day,
family::Family, log::Log, logbook::Logbook, mail::Mail, notification::Notification, role::Role,
stat::Stat, tripdetails::TripDetails, Day,
};
use crate::{
tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, BOAT_STORAGE,
EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG,
SCHECKBUCH, STUDENT_OR_PUPIL, UNTERSTUETZEND,
};
use crate::tera::admin::user::UserEditForm;
const RENNRUDERBEITRAG: i32 = 11000;
const BOAT_STORAGE: i32 = 4500;
const FAMILY_TWO: i32 = 30000;
const FAMILY_THREE_OR_MORE: i32 = 35000;
const STUDENT_OR_PUPIL: i32 = 8000;
const REGULAR: i32 = 22000;
const UNTERSTUETZEND: i32 = 2500;
const FOERDERND: i32 = 8500;
pub const SCHECKBUCH: i32 = 3000;
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
pub struct User {
@ -48,6 +42,7 @@ pub struct User {
pub phone: Option<String>,
pub address: Option<String>,
pub family_id: Option<i64>,
pub user_token: String,
}
#[derive(Debug, Serialize, Deserialize)]
@ -256,6 +251,35 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
Ok(())
}
async fn send_end_mail_scheckbuch(
&self,
db: &mut Transaction<'_, Sqlite>,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
Mail::send_single_tx(
db,
mail,
"ASKÖ Ruderverein Donau Linz | Deine Mitgliedschaft wartet auf Dich",
format!(
"Hallo {0},
herzlichen Glückwunsch---Du hast Deine fünf Scheckbuch-Ausfahrten erfolgreich absolviert! Wir hoffen, dass Du das Rudern bei uns genauso genossen hast wie wir es genossen haben, Dich auf dem Wasser zu begleiten.
Wir würden uns sehr freuen, Dich als festes Mitglied in unserem Verein willkommen zu heißen! Als Mitglied stehen Dir dann alle unsere Ausfahrten offen, die von unseren Steuerleuten organisiert werden. Im Sommer erwarten Dich zusätzlich spannende Events: Wanderfahrten, Sternfahrten, Fetzenfahrt, .... Im Winter bieten wir Indoor-Ergo-Challenges an, bei denen Du Deine Fitness auf dem Ruderergometer unter Beweis stellen kannst. Alle Details zu diesen Aktionen erfährst Du, sobald Du Teil unseres Vereins bist! :-)
Alle Informationen zu den Mitgliedsbeiträgen findest Du unter https://rudernlinz.at/unser-verein/gebuhren/ Falls Du Dich entscheidest, unserem Verein beizutreten, fülle bitte unser Beitrittsformular auf https://rudernlinz.at/unser-verein/downloads/ aus und sende es an info@rudernlinz.at.
Wir freuen uns, Dich bald wieder auf dem Wasser zu sehen.
Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
Ok(())
}
async fn send_welcome_mail_full_member(
&self,
db: &SqlitePool,
@ -273,15 +297,15 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at und it@rudernlinz.at jederzeit zur Verfügung.
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{0}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden.
Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst.
Beim nächsten Treffen im Verein, erinnere jemand vom Vorstand (https://rudernlinz.at/unser-verein/vorstand/) bitte daran, deinen Fingerabdruck zu registrieren, damit du Zugang zum Bootshaus erhältst.
Außerdem haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz'. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
@ -361,17 +385,49 @@ ASKÖ Ruderverein Donau Linz", self.name),
);
}
if let Some(member_since_date) = &self.member_since_date {
if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{
if member_since_date.year() == Local::now().year()
&& !self.has_role(db, "no-einschreibgebuehr").await
{
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR);
}
}
}
let halfprice = if let Some(member_since_date) = &self.member_since_date {
if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{
let halfprice_startdate =
NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap();
member_since_date >= halfprice_startdate
} else {
false
}
} else {
false
};
if self.has_role(db, "Unterstützend").await {
fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND);
} else if self.has_role(db, "Förderndes Mitglied").await {
fee.add("Förderndes Mitglied".into(), FOERDERND);
} else if Family::find_by_opt_id(db, self.family_id).await.is_none() {
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
if halfprice {
fee.add("Schüler/Student (Halbpreis)".into(), STUDENT_OR_PUPIL / 2);
} else {
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
}
} else if self.has_role(db, "Ehrenmitglied").await {
fee.add("Ehrenmitglied".into(), 0);
} else {
fee.add("Mitgliedsbeitrag".into(), REGULAR);
if halfprice {
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
} else {
fee.add("Mitgliedsbeitrag".into(), REGULAR);
}
}
}
@ -417,6 +473,12 @@ ASKÖ Ruderverein Donau Linz", self.name),
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 has_membership_pdf(&self, db: &SqlitePool) -> bool {
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
.fetch_one(db)
@ -461,7 +523,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
FROM user
WHERE id like ?
",
@ -476,7 +538,7 @@ WHERE id like ?
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
FROM user
WHERE id like ?
",
@ -493,7 +555,7 @@ WHERE id like ?
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
FROM user
WHERE lower(name)=?
",
@ -535,7 +597,7 @@ WHERE lower(name)=?
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
FROM user
WHERE deleted = 0
ORDER BY last_access DESC
@ -557,7 +619,7 @@ ORDER BY last_access DESC
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
FROM user u
JOIN user_role ur ON u.id = ur.user_id
WHERE ur.role_id = ? AND deleted = 0
@ -573,14 +635,14 @@ ORDER BY name;
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user
WHERE family_id IS NOT NULL
GROUP BY family_id
UNION
-- Select users with a null family_id, without grouping
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user
WHERE family_id IS NULL;
"
)
@ -593,7 +655,7 @@ WHERE family_id IS NULL;
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
FROM user
WHERE deleted = 0 AND dob != '' and weight != '' and sex != ''
ORDER BY name
@ -608,7 +670,7 @@ ORDER BY name
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
FROM user
WHERE 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
@ -627,6 +689,14 @@ ORDER BY last_access DESC
.is_ok()
}
pub async fn create_with_mail(db: &SqlitePool, name: &str, mail: &str) -> bool {
let name = name.trim();
sqlx::query!("INSERT INTO USER(name, mail) VALUES (?, ?)", name, mail)
.execute(db)
.await
.is_ok()
}
pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) {
let mut family_id = data.family_id;
@ -714,6 +784,7 @@ ORDER BY last_access DESC
if ![
"n-sageder",
"p-hofer",
"marie-birner",
"daniel-kortschak",
"rudernlinz",
"m-birner",
@ -837,18 +908,32 @@ ORDER BY last_access DESC
days
}
async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
pub(crate) async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
if self.has_role(db, "cox").await {
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok,
//december
//has 31
//days
end_of_year
let days_left_in_year = end_of_year
.signed_duration_since(Local::now().date_naive())
.num_days()
+ 1
+ 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 {
6
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD
}
}
@ -864,6 +949,49 @@ ORDER BY last_access DESC
}
None
}
pub(crate) async fn received_new_logentry(
&self,
db: &mut Transaction<'_, Sqlite>,
smtp_pw: &str,
) {
if self.has_role_tx(db, "scheckbuch").await {
let amount_trips = Logbook::completed_with_user_tx(db, &self).await.len();
if amount_trips == 5 {
if let Some(mail) = &self.mail {
let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await;
}
let coxes = Role::find_by_name_tx(db, "cox").await.unwrap();
Notification::create_for_role_tx(
db,
&coxes,
&format!(
"Liebe Steuerberechtigte, {} hat alle Ausfahrten des Scheckbuchs absolviert. Hoffentlich können wir uns bald über ein neues Mitglied freuen :-)",
self.name
),
"Scheckbuch fertig",
None,None
)
.await;
} else if amount_trips > 5 {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&board,
&format!(
"Lieber Vorstand, {} hat nun bereits die {}. seiner 5 Scheckbuchausfahrten absolviert.",
self.name, amount_trips
),
"Scheckbuch überfertig",
None,None
)
.await;
}
}
// TODO: check fahrtenabzeichen fertig?
// TODO: check äquatorpreis geschafft?
}
}
#[async_trait]
@ -896,266 +1024,84 @@ impl<'r> FromRequest<'r> for User {
}
}
pub struct TechUser {
pub(crate) user: User,
}
/// 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 TechUser {
type Target = User;
impl Deref for $name {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.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 TechUser {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
Outcome::Success(user) => {
if user.has_role(db, "tech").await {
Outcome::Success(TechUser { user })
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
#[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),
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
pub struct CoxUser {
pub(crate) user: User,
}
impl Deref for CoxUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.user
}
}
impl CoxUser {
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
if user.has_role(db, "cox").await {
Some(CoxUser { user })
} else {
None
}
}
}
#[async_trait]
impl<'r> FromRequest<'r> for CoxUser {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
Outcome::Success(user) => {
if user.has_role(db, "cox").await {
Outcome::Success(CoxUser { user })
impl $name {
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
if special_user!(@check_roles user, db, $($role)*) {
Some($name { user })
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
None
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminUser {
pub(crate) user: User,
}
#[async_trait]
impl<'r> FromRequest<'r> for AdminUser {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
Outcome::Success(user) => {
if user.has_role(db, "admin").await {
Outcome::Success(AdminUser { user })
} else {
Outcome::Forward(Status::Forbidden)
};
(@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;
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
)*
has_positive_role
$(
&& !$user.has_role($db, $neg_role).await
)*
}
}
};
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AllowedForPlannedTripsUser(pub(crate) User);
special_user!(TechUser, +"tech");
special_user!(CoxUser, +"cox");
special_user!(AdminUser, +"admin");
special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch");
special_user!(DonauLinzUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied");
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
special_user!(VorstandUser, +"Vorstand");
special_user!(EventUser, +"manage_events");
special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin");
special_user!(ManageUserUser, +"admin", +"schriftfuehrer");
special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin");
#[async_trait]
impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser {
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 user.has_role(db, "Donau Linz").await | user.has_role(db, "scheckbuch").await {
Outcome::Success(AllowedForPlannedTripsUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
impl From<AllowedForPlannedTripsUser> for User {
fn from(val: AllowedForPlannedTripsUser) -> Self {
val.0
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DonauLinzUser(pub(crate) User);
impl From<DonauLinzUser> for User {
fn from(val: DonauLinzUser) -> Self {
val.0
}
}
impl Deref for DonauLinzUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for DonauLinzUser {
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 user.has_role(db, "Donau Linz").await
&& !user.has_role(db, "Unterstützend").await
&& !user.has_role(db, "Förderndes Mitglied").await
{
Outcome::Success(DonauLinzUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SchnupperBetreuerUser(pub(crate) User);
impl From<SchnupperBetreuerUser> for User {
fn from(val: SchnupperBetreuerUser) -> Self {
val.0
}
}
impl Deref for SchnupperBetreuerUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for SchnupperBetreuerUser {
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 user.has_role(db, "schnupper-betreuer").await {
Outcome::Success(SchnupperBetreuerUser(user))
} else {
Outcome::Forward(Status::Forbidden)
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VorstandUser(pub(crate) User);
impl From<VorstandUser> for User {
fn from(val: VorstandUser) -> Self {
val.0
}
}
impl Deref for VorstandUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for VorstandUser {
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 user.has_role(db, "Vorstand").await {
Outcome::Success(VorstandUser(user))
} else {
Outcome::Forward(Status::Forbidden)
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EventUser(pub(crate) User);
impl From<EventUser> for User {
fn from(val: EventUser) -> Self {
val.0
}
}
impl Deref for EventUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
pub struct UserWithRolesAndMembershipPdf {
#[serde(flatten)]
@ -1199,25 +1145,6 @@ impl UserWithMembershipPdf {
}
}
#[async_trait]
impl<'r> FromRequest<'r> for EventUser {
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 user.has_role(db, "manage_events").await {
Outcome::Success(EventUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
@ -1266,7 +1193,7 @@ mod test {
fn test_cox() {
let pool = testdb!();
let res = User::cox(&pool).await;
assert_eq!(res.len(), 3);
assert_eq!(res.len(), 4);
}
#[sqlx::test]

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

View File

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

View File

@ -18,6 +18,7 @@ use crate::model::{
struct AddEventForm<'r> {
name: &'r str,
planned_amount_cox: i32,
always_show: bool,
tripdetails: TripDetailsToAdd<'r>,
}
@ -34,7 +35,14 @@ async fn create(
//just created
//the object
Event::create(db, data.name, data.planned_amount_cox, &trip_details).await;
Event::create(
db,
data.name,
data.planned_amount_cox,
data.always_show,
&trip_details,
)
.await;
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
}

View File

@ -9,14 +9,14 @@ 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::model::user::{AdminUser, VorstandUser};
use crate::tera::Config;
#[get("/mail")]
async fn index(
db: &State<SqlitePool>,
admin: AdminUser,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
@ -65,7 +65,7 @@ async fn update(
db: &State<SqlitePool>,
data: Form<MailToSend<'_>>,
config: &State<Config>,
admin: AdminUser,
admin: VorstandUser,
) -> Flash<Redirect> {
let d = data.into_inner();
Log::create(db, format!("{admin:?} trying to send this mail: {d:?}")).await;

View File

@ -2,7 +2,7 @@ use crate::model::{
log::Log,
notification::Notification,
role::Role,
user::{AdminUser, User, UserWithDetails},
user::{User, UserWithDetails, VorstandUser},
};
use itertools::Itertools;
use rocket::{
@ -18,7 +18,7 @@ use sqlx::SqlitePool;
#[get("/notification")]
async fn index(
db: &State<SqlitePool>,
user: AdminUser,
user: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
@ -62,7 +62,7 @@ pub struct NotificationToSendUser {
async fn send_group(
db: &State<SqlitePool>,
data: Form<NotificationToSendGroup>,
admin: AdminUser,
admin: VorstandUser,
) -> Flash<Redirect> {
let d = data.into_inner();
Log::create(
@ -89,7 +89,7 @@ async fn send_group(
async fn send_user(
db: &State<SqlitePool>,
data: Form<NotificationToSendUser>,
admin: AdminUser,
admin: VorstandUser,
) -> Flash<Redirect> {
let d = data.into_inner();
Log::create(

View File

@ -29,7 +29,7 @@ async fn index(
context.insert("schnupperanten", &users);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.user, db).await,
);
Template::render("admin/schnupper/index", context.into_json())

View File

@ -7,13 +7,14 @@ use crate::{
logbook::Logbook,
role::Role,
user::{
AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
VorstandUser,
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, SchnupperBetreuerUser, User,
UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
},
},
tera::Config,
};
use futures::future::join_all;
use lettre::Address;
use rocket::{
form::Form,
fs::TempFile,
@ -54,8 +55,8 @@ async fn index(
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
.collect();
let user: User = user.into();
let allowed_to_edit = user.has_role(db, "admin").await;
let user: User = user.into_inner();
let allowed_to_edit = ManageUserUser::new(db, user.clone()).await.is_some();
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
@ -89,7 +90,7 @@ async fn index_admin(
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
let user: User = user.user;
let allowed_to_edit = user.has_role(db, "admin").await;
let allowed_to_edit = ManageUserUser::new(db, user.clone()).await.is_some();
let roles = Role::all(db).await;
let families = Family::all_with_members(db).await;
@ -110,7 +111,7 @@ async fn index_admin(
#[get("/user/fees")]
async fn fees(
db: &State<SqlitePool>,
admin: VorstandUser,
user: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
@ -130,7 +131,7 @@ async fn fees(
}
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.into(), db).await,
&UserWithDetails::from_user(user.into_inner(), db).await,
);
Template::render("admin/user/fees", context.into_json())
@ -161,7 +162,7 @@ async fn scheckbuch(
}
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.into_inner(), db).await,
);
Template::render("admin/user/scheckbuch", context.into_json())
@ -170,7 +171,7 @@ async fn scheckbuch(
#[get("/user/fees/paid?<user_ids>")]
async fn fees_paid(
db: &State<SqlitePool>,
admin: AdminUser,
calling_user: AllowedToEditPaymentStatusUser,
user_ids: Vec<i32>,
referer: Referer,
) -> Flash<Redirect> {
@ -181,7 +182,10 @@ async fn fees_paid(
if user.has_role(db, "paid").await {
Log::create(
db,
format!("{} set fees NOT paid for '{}'", admin.user.name, user.name),
format!(
"{} set fees NOT paid for '{}'",
calling_user.user.name, user.name
),
)
.await;
user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap())
@ -189,7 +193,10 @@ async fn fees_paid(
} else {
Log::create(
db,
format!("{} set fees paid for '{}'", admin.user.name, user.name),
format!(
"{} set fees paid for '{}'",
calling_user.user.name, user.name
),
)
.await;
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
@ -208,7 +215,7 @@ async fn fees_paid(
#[get("/user/<user>/send-welcome-mail")]
async fn send_welcome_mail(
db: &State<SqlitePool>,
_admin: AdminUser,
_admin: ManageUserUser,
config: &State<Config>,
user: i32,
) -> Flash<Redirect> {
@ -226,7 +233,7 @@ async fn send_welcome_mail(
}
#[get("/user/<user>/reset-pw")]
async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await;
match user {
Some(user) => {
@ -246,7 +253,7 @@ async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<R
}
#[get("/user/<user>/delete")]
async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await;
Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await;
match user {
@ -283,7 +290,7 @@ pub struct UserEditForm<'a> {
async fn update(
db: &State<SqlitePool>,
data: Form<UserEditForm<'_>>,
admin: AdminUser,
admin: ManageUserUser,
) -> Flash<Redirect> {
let user = User::find_by_id(db, data.id).await;
Log::create(
@ -306,7 +313,7 @@ async fn update(
#[get("/user/<user>/membership")]
async fn download_membership_pdf(
db: &State<SqlitePool>,
admin: AdminUser,
admin: ManageUserUser,
user: i32,
) -> (ContentType, Vec<u8>) {
let user = User::find_by_id(db, user).await.unwrap();
@ -332,7 +339,7 @@ struct UserAddForm<'r> {
async fn create(
db: &State<SqlitePool>,
data: Form<UserAddForm<'_>>,
admin: AdminUser,
admin: ManageUserUser,
) -> Flash<Redirect> {
if User::create(db, data.name).await {
Log::create(
@ -349,6 +356,103 @@ async fn create(
}
}
#[derive(FromForm, Debug)]
struct UserAddScheckbuchForm<'r> {
name: &'r str,
mail: &'r str,
}
#[post("/user/new/scheckbuch", data = "<data>")]
async fn create_scheckbuch(
db: &State<SqlitePool>,
data: Form<UserAddScheckbuchForm<'_>>,
admin: VorstandUser,
config: &State<Config>,
) -> Flash<Redirect> {
// 1. Check mail adress
let mail = data.mail.trim();
if mail.parse::<Address>().is_err() {
return Flash::error(
Redirect::to("/admin/user/scheckbuch"),
"Keine gültige Mailadresse".to_string(),
);
}
// 2. Check name
let name = data.name.trim();
if User::find_by_name(db, name).await.is_some() {
return Flash::error(
Redirect::to("/admin/user/scheckbuch"),
"Kann kein Scheckbuch erstellen, der Name wird bereits von einem User verwendet"
.to_string(),
);
}
// 3. Create user
User::create_with_mail(db, name, mail).await;
let user = User::find_by_name(db, name).await.unwrap();
// 4. Add 'scheckbuch' role
let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
user.add_role(db, &scheckbuch).await;
// 4. Send welcome mail (+ notification)
user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
Log::create(
db,
format!("{} created new scheckbuch: {data:?}", admin.name),
)
.await;
Flash::success(Redirect::to("/admin/user/scheckbuch"), &format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt."))
}
#[get("/user/move/schnupperant/<id>/to/scheckbuch")]
async fn schnupper_to_scheckbuch(
db: &State<SqlitePool>,
id: i32,
admin: SchnupperBetreuerUser,
config: &State<Config>,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/schnupper"),
"user id not found".to_string(),
);
};
if !user.has_role(db, "schnupperant").await {
return Flash::error(
Redirect::to("/admin/schnupper"),
"kein schnupperant...".to_string(),
);
}
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
let paid = Role::find_by_name(db, "paid").await.unwrap();
user.remove_role(db, &schnupperant).await;
user.remove_role(db, &paid).await;
let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
user.add_role(db, &scheckbuch).await;
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
user.add_role(db, &no_einschreibgebuehr).await;
}
user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
Log::create(
db,
format!(
"{} created new scheckbuch (from schnupperant): {}",
admin.name, user.name
),
)
.await;
Flash::success(Redirect::to("/admin/schnupper"), &format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap()))
}
pub fn routes() -> Vec<Route> {
routes![
index,
@ -356,6 +460,8 @@ pub fn routes() -> Vec<Route> {
resetpw,
update,
create,
create_scheckbuch,
schnupper_to_scheckbuch,
delete,
fees,
fees_paid,

View File

@ -0,0 +1,46 @@
use crate::model::{
personal::Achievements,
role::Role,
user::{User, UserWithDetails, VorstandUser},
};
use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
#[get("/achievement")]
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 role = Role::find_by_name(&db, "Donau Linz").await.unwrap();
let users = User::all_with_role(&db, &role).await;
let mut people = Vec::new();
let mut rowingbadge_year = None;
for user in users {
let achievement = Achievements::for_user(&db, &user).await;
if let Some(badge) = &achievement.rowingbadge {
rowingbadge_year = Some(badge.year);
}
people.push((user, achievement));
}
context.insert("people", &people);
context.insert("rowingbadge_year", &rowingbadge_year.unwrap());
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.into_inner(), db).await,
);
Template::render("achievement", context.into_json())
}
pub fn routes() -> Vec<Route> {
routes![index]
}

View File

@ -39,7 +39,7 @@ async fn index(
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.into(), db).await,
&UserWithDetails::from_user(admin.into_inner(), db).await,
);
Template::render("board/boathouse", context.into_json())

View File

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

View File

@ -59,7 +59,7 @@ async fn index(
context.insert("boats", &boats);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.into_inner(), db).await,
);
Template::render("boatdamages", context.into_json())
@ -78,7 +78,7 @@ async fn create<'r>(
data: Form<FormBoatDamageToAdd<'r>>,
user: DonauLinzUser,
) -> Flash<Redirect> {
let user: User = user.into();
let user: User = user.into_inner();
let boatdamage_to_add = BoatDamageToAdd {
boat_id: data.boat_id,
desc: data.desc,

View File

@ -75,7 +75,7 @@ async fn index(
context.insert("user", &User::all(db).await);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.into_inner(), db).await,
);
Template::render("boatreservations", context.into_json())
@ -97,7 +97,7 @@ async fn create<'r>(
data: Form<FormBoatReservationToAdd<'r>>,
user: DonauLinzUser,
) -> Flash<Redirect> {
let user_applicant: User = user.into();
let user_applicant: User = user.into_inner();
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
let boatreservation_to_add = BoatReservationToAdd {
boat: &boat,

View File

@ -11,7 +11,7 @@ use crate::model::{
log::Log,
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
tripdetails::{TripDetails, TripDetailsToAdd},
user::CoxUser,
user::{AllowedToUpdateTripToAlwaysBeShownUser, CoxUser},
};
#[post("/trip", data = "<data>")]
@ -42,7 +42,6 @@ struct EditTripForm<'r> {
max_people: i32,
notes: Option<&'r str>,
trip_type: Option<i64>,
always_show: bool,
is_locked: bool,
}
@ -60,7 +59,6 @@ async fn update(
max_people: data.max_people,
notes: data.notes,
trip_type: data.trip_type,
always_show: data.always_show,
is_locked: data.is_locked,
};
match Trip::update_own(db, &update).await {
@ -80,6 +78,23 @@ async fn update(
}
}
#[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("/planned"),
"'Immer anzeigen' erfolgreich gesetzt!",
)
} else {
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
}
}
#[get("/join/<planned_event_id>")]
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
@ -164,12 +179,19 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
}
pub fn routes() -> Vec<Route> {
routes![create, join, remove, remove_trip, update]
routes![
create,
join,
remove,
remove_trip,
update,
toggle_always_show
]
}
#[cfg(test)]
mod test {
use chrono::NaiveDate;
use chrono::{Local, NaiveDate};
use rocket::{
http::{ContentType, Status},
local::asynchronous::Client,
@ -230,7 +252,9 @@ mod test {
fn test_trip_update_succ() {
let db = testdb!();
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
println!("{tomorrow}");
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(1, trip.trip.max_people);
assert_eq!(
"trip_details for trip from cox",
@ -266,7 +290,8 @@ mod test {
"7:successAusfahrt erfolgreich aktualisiert."
);
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(12, trip.trip.max_people);
assert_eq!("my-new-notes", &trip.trip.notes.clone().unwrap());
}
@ -306,7 +331,9 @@ mod test {
fn test_trip_update_wrong_cox() {
let db = testdb!();
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(1, trip.trip.max_people);
assert_eq!(
"trip_details for trip from cox",

View File

@ -15,16 +15,21 @@ use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool;
use tera::Context;
use crate::model::{
boat::Boat,
boatreservation::BoatReservation,
log::Log,
logbook::{
LogToAdd, LogToFinalize, Logbook, LogbookCreateError, LogbookDeleteError,
LogbookUpdateError,
use crate::{
model::{
boat::Boat,
boatreservation::BoatReservation,
distance::Distance,
log::Log,
logbook::{
LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError,
LogbookCreateError, LogbookDeleteError, LogbookUpdateError,
},
logtype::LogType,
trip::Trip,
user::{AdminUser, DonauLinzUser, User, UserWithDetails, VorstandUser},
},
logtype::LogType,
user::{AdminUser, DonauLinzUser, User, UserWithDetails},
tera::Config,
};
pub struct KioskCookie(());
@ -68,11 +73,13 @@ async fn index(
)
.await;
users.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
u.roles.contains(&"Donau Linz".into())
|| u.roles.contains(&"scheckbuch".into())
|| u.user.name == "Externe Steuerperson"
});
let logtypes = LogType::all(db).await;
let distances = Logbook::distances(db).await;
let distances = Distance::all(db).await;
let on_water = Logbook::on_water(db).await;
@ -82,6 +89,7 @@ async fn index(
}
context.insert("boats", &boats);
context.insert("planned_trips", &Trip::get_for_today(db).await);
context.insert(
"reservations",
&BoatReservation::all_future_with_groups(db).await,
@ -91,7 +99,7 @@ async fn index(
context.insert("logtypes", &logtypes);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.into_inner(), db).await,
);
context.insert("on_water", &on_water);
context.insert("distances", &distances);
@ -105,7 +113,7 @@ async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
Template::render(
"log.completed",
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into(), db).await),
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await),
)
}
@ -176,7 +184,7 @@ async fn kiosk(
});
let logtypes = LogType::all(db).await;
let distances = Logbook::distances(db).await;
let distances = Distance::all(db).await;
let on_water = Logbook::on_water(db).await;
@ -185,6 +193,7 @@ async fn kiosk(
context.insert("flash", &msg.into_inner());
}
context.insert("planned_trips", &Trip::get_for_today(db).await);
context.insert("boats", &boats);
context.insert(
"reservations",
@ -204,11 +213,12 @@ async fn create_logbook(
db: &SqlitePool,
data: Form<LogToAdd>,
user: &DonauLinzUser,
smtp_pw: &str,
) -> Flash<Redirect> {
match Logbook::create(
db,
data.into_inner(),
user
user, smtp_pw
)
.await
{
@ -228,6 +238,8 @@ async fn create_logbook(
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
Err(LogbookCreateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
}
}
@ -236,6 +248,7 @@ async fn create(
db: &State<SqlitePool>,
data: Form<LogToAdd>,
user: DonauLinzUser,
config: &State<Config>,
) -> Flash<Redirect> {
Log::create(
db,
@ -243,7 +256,7 @@ async fn create(
)
.await;
create_logbook(db, data, &user).await
create_logbook(db, data, &user, &config.smtp_pw).await
}
#[post("/", data = "<data>")]
@ -251,6 +264,7 @@ async fn create_kiosk(
db: &State<SqlitePool>,
data: Form<LogToAdd>,
_kiosk: KioskCookie,
config: &State<Config>,
) -> Flash<Redirect> {
let Some(boat) = Boat::find_by_id(db, data.boat_id).await else {
return Flash::error(Redirect::to("/log"), "Boot gibt's nicht");
@ -279,14 +293,57 @@ async fn create_kiosk(
)
.await;
create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme
create_logbook(
db,
data,
&DonauLinzUser::new(db, creator).await.unwrap(),
&config.smtp_pw,
)
.await
//TODO: fixme
}
#[post("/update", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<LogToUpdate>,
user: VorstandUser,
) -> Flash<Redirect> {
let data = data.into_inner();
let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
return Flash::error(Redirect::to("/log"), &format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
};
match logbook.update(db, data.clone(), &user.user).await {
Ok(()) => {
Log::create(
db,
format!(
"User {} updated log entry={:?} to {:?}",
&user.name, logbook, data
),
)
.await;
Flash::success(
Redirect::to("/log/show"),
"Logbucheintrag erfolgreich bearbeitet".to_string(),
)
}
Err(LogbookAdminUpdateError::NotAllowed) => Flash::error(
Redirect::to("/log/show"),
"Du hast keine Erlaubnis, diesen Logbucheintrag zu bearbeiten!".to_string(),
),
}
}
async fn home_logbook(
db: &SqlitePool,
data: Form<LogToFinalize>,
logbook_id: i32,
logbook_id: i64,
user: &DonauLinzUser,
smtp_pw: &str,
) -> Flash<Redirect> {
let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await;
let Some(logbook) = logbook else {
@ -296,11 +353,14 @@ async fn home_logbook(
);
};
match logbook.home(db,user, data.into_inner()).await {
match logbook.home(db,user, data.into_inner(), smtp_pw).await {
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit."),
Err(e) => Flash::error(
Redirect::to("/log"),
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
@ -312,8 +372,9 @@ async fn home_logbook(
async fn home_kiosk(
db: &State<SqlitePool>,
data: Form<LogToFinalize>,
logbook_id: i32,
logbook_id: i64,
_kiosk: KioskCookie,
config: &State<Config>,
) -> Flash<Redirect> {
let logbook = Logbook::find_by_id(db, logbook_id).await.unwrap(); //TODO: fixme
@ -327,11 +388,15 @@ async fn home_kiosk(
db,
data,
logbook_id,
&DonauLinzUser(
&DonauLinzUser::new(
db,
User::find_by_id(db, logbook.shipmaster as i32)
.await
.unwrap(),
), //TODO: fixme
)
.await
.unwrap(),
&config.smtp_pw,
)
.await
}
@ -340,8 +405,9 @@ async fn home_kiosk(
async fn home(
db: &State<SqlitePool>,
data: Form<LogToFinalize>,
logbook_id: i32,
logbook_id: i64,
user: DonauLinzUser,
config: &State<Config>,
) -> Flash<Redirect> {
Log::create(
db,
@ -352,13 +418,18 @@ async fn home(
)
.await;
home_logbook(db, data, logbook_id, &user).await
home_logbook(db, data, logbook_id, &user, &config.smtp_pw).await
}
#[get("/<logbook_id>/delete", rank = 2)]
async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) -> Flash<Redirect> {
async fn delete(db: &State<SqlitePool>, logbook_id: i64, user: DonauLinzUser) -> Flash<Redirect> {
let logbook = Logbook::find_by_id(db, logbook_id).await;
if let Some(logbook) = logbook {
let redirect = if logbook.arrival.is_some() {
"/log/show"
} else {
"/log"
};
Log::create(
db,
format!("User {} tries to delete log entry {logbook_id}", &user.name),
@ -366,11 +437,11 @@ async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) ->
.await;
match logbook.delete(db, &user).await {
Ok(_) => Flash::success(
Redirect::to("/log"),
format!("Eintrag {} gelöscht!", logbook_id),
Redirect::to(redirect),
format!("Eintrag {} von {} gelöscht!", logbook_id, user.name),
),
Err(LogbookDeleteError::NotYourEntry) => Flash::error(
Redirect::to("/log"),
Redirect::to(redirect),
"Du hast nicht die Berechtigung, den Eintrag zu löschen!",
),
}
@ -385,7 +456,7 @@ async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) ->
#[get("/<logbook_id>/delete")]
async fn delete_kiosk(
db: &State<SqlitePool>,
logbook_id: i32,
logbook_id: i64,
_kiosk: KioskCookie,
) -> Flash<Redirect> {
let logbook = Logbook::find_by_id(db, logbook_id).await;
@ -425,7 +496,8 @@ pub fn routes() -> Vec<Route> {
show_kiosk,
show_for_year,
delete,
delete_kiosk
delete_kiosk,
update
]
}

View File

@ -1,7 +1,7 @@
use rocket::{get, http::ContentType, routes, Route, State};
use sqlx::SqlitePool;
use crate::model::event::Event;
use crate::model::{event::Event, personal::cal::get_personal_cal, user::User};
#[get("/cal")]
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
@ -9,8 +9,25 @@ async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
(ContentType::Calendar, Event::get_ics_feed(db).await)
}
#[get("/cal/personal/<user_id>/<uuid>")]
async fn cal_registered(
db: &State<SqlitePool>,
user_id: i32,
uuid: &str,
) -> Result<(ContentType, String), String> {
let Some(user) = User::find_by_id(db, user_id).await else {
return Err("Invalid".into());
};
if &user.user_token != uuid {
return Err("Invalid".into());
}
Ok((ContentType::Calendar, get_personal_cal(db, &user).await))
}
pub fn routes() -> Vec<Route> {
routes![cal]
routes![cal, cal_registered]
}
#[cfg(test)]

View File

@ -20,11 +20,15 @@ use serde::Deserialize;
use sqlx::SqlitePool;
use tera::Context;
use crate::model::{
logbook::Logbook,
notification::Notification,
role::Role,
user::{User, UserWithDetails, SCHECKBUCH},
use crate::{
model::{
logbook::Logbook,
notification::Notification,
personal::Achievements,
role::Role,
user::{User, UserWithDetails},
},
SCHECKBUCH,
};
pub(crate) mod admin;
@ -59,6 +63,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
context.insert("last_trips", &last_trips);
}
context.insert("achievements", &Achievements::for_user(db, &user).await);
context.insert("notifications", &Notification::for_user(db, &user).await);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("costs_scheckbuch", &SCHECKBUCH);
@ -118,6 +123,60 @@ fn unauthorized_error(req: &Request) -> Redirect {
Redirect::to("/auth")
}
#[derive(FromForm, Debug)]
struct NewBlogpostForm<'r> {
article_url: &'r str,
article_title: &'r str,
pw: &'r str,
}
#[post("/", data = "<blogpost>")]
async fn new_blogpost(
db: &State<SqlitePool>,
blogpost: Form<NewBlogpostForm<'_>>,
config: &State<Config>,
) -> String {
if blogpost.pw == config.wordpress_key {
let member = Role::find_by_name(db, "Donau Linz").await.unwrap();
Notification::create_for_role(
db,
&member,
&urlencoding::decode(blogpost.article_title).expect("UTF-8"),
"Neuer Blogpost",
Some(blogpost.article_url),
None,
)
.await;
"ACK".into()
} else {
"WRONG pw".into()
}
}
#[derive(FromForm, Debug)]
struct BlogpostUnpublishedForm<'r> {
article_url: &'r str,
pw: &'r str,
}
#[post("/", data = "<blogpost>")]
async fn blogpost_unpublished(
db: &State<SqlitePool>,
blogpost: Form<BlogpostUnpublishedForm<'_>>,
config: &State<Config>,
) -> String {
if blogpost.pw == config.wordpress_key {
Notification::delete_by_link(
db,
&urlencoding::decode(blogpost.article_url).expect("UTF-8"),
)
.await;
"ACK".into()
} else {
"WRONG pw".into()
}
}
#[catch(403)] //forbidden
fn forbidden_error() -> Flash<Redirect> {
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
@ -187,6 +246,7 @@ pub struct Config {
smtp_pw: String,
usage_log_path: String,
pub openweathermap_key: String,
wordpress_key: String,
}
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
@ -194,6 +254,8 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
.mount("/", routes![index, steering, impressum])
.mount("/auth", auth::routes())
.mount("/wikiauth", routes![wikiauth])
.mount("/new-blogpost", routes![new_blogpost])
.mount("/blogpost-unpublished", routes![blogpost_unpublished])
.mount("/log", log::routes())
.mount("/planned", planned::routes())
.mount("/ergo", ergo::routes())

View File

@ -8,12 +8,15 @@ use rocket_dyn_templates::Template;
use sqlx::SqlitePool;
use tera::Context;
use crate::model::{
log::Log,
tripdetails::TripDetails,
triptype::TripType,
user::{AllowedForPlannedTripsUser, User, UserWithDetails},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
use crate::{
model::{
log::Log,
tripdetails::TripDetails,
triptype::TripType,
user::{AllowedForPlannedTripsUser, User, UserWithDetails},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
},
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
};
#[get("/")]
@ -22,7 +25,7 @@ async fn index(
user: AllowedForPlannedTripsUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let user: User = user.into();
let user: User = user.into_inner();
let mut context = Context::new();
@ -37,7 +40,15 @@ async fn index(
context.insert("flash", &msg.into_inner());
}
context.insert(
"allowed_to_update_always_show_trip",
&user.allowed_to_update_always_show_trip(db).await,
);
context.insert("fee", &user.fee(db).await);
context.insert(
"amount_days_to_show_trips_ahead",
&AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("days", &days);
Template::render("planned", context.into_json())
@ -50,7 +61,7 @@ async fn join(
user: AllowedForPlannedTripsUser,
user_note: Option<String>,
) -> Flash<Redirect> {
let user: User = user.into();
let user: User = user.into_inner();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
@ -99,6 +110,10 @@ async fn join(
Redirect::to("/planned"),
"Du darfst keine Gäste hinzufügen.",
),
Err(UserTripError::NotVisibleToUser) => Flash::error(
Redirect::to("/planned"),
"Du kannst dich nicht registrieren, weil du die Ausfahrt gar nicht sehen solltest.",
),
Err(UserTripError::DetailsLocked) => Flash::error(
Redirect::to("/planned"),
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.",
@ -113,7 +128,7 @@ async fn remove_guest(
user: AllowedForPlannedTripsUser,
name: String,
) -> Flash<Redirect> {
let user: User = user.into();
let user: User = user.into_inner();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
@ -147,6 +162,10 @@ async fn remove_guest(
Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
}
Err(UserTripDeleteError::NotVisibleToUser) => Flash::error(
Redirect::to("/planned"),
"Du kannst dich nicht abmelden, weil du die Ausfahrt gar nicht sehen solltest.",
),
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
Redirect::to("/planned"),
"Keine Berechtigung um den Gast zu entfernen.",
@ -160,7 +179,7 @@ async fn remove(
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
) -> Flash<Redirect> {
let user: User = user.into();
let user: User = user.into_inner();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
@ -191,6 +210,18 @@ async fn remove(
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(UserTripDeleteError::NotVisibleToUser) => {
Log::create(
db,
format!(
"User {} tried to unregister for not-yet-seeable trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/planned"), "Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...")
}
Err(_) => {
panic!("Not possible to be here");
}

View File

@ -16,7 +16,7 @@ async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
Template::render(
"stat.boats",
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, kiosk),
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, kiosk),
)
}
@ -38,7 +38,7 @@ async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -
Template::render(
"stat.people",
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, personal, kiosk, guest_km, club_km),
)
}

View File

@ -59,7 +59,7 @@ async fn index(
context.insert("user", &User::all(db).await);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.into_inner(), db).await,
);
Template::render("trailerreservations", context.into_json())
@ -81,7 +81,7 @@ async fn create<'r>(
data: Form<FormTrailerReservationToAdd<'r>>,
user: DonauLinzUser,
) -> Flash<Redirect> {
let user_applicant: User = user.into();
let user_applicant: User = user.into_inner();
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
.await
.unwrap();

View File

@ -0,0 +1,100 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/log" as log %}
{% extends "base" %}
{% block content %}
<link rel="stylesheet" href="/public/table.css" />
<div class="w-full">
<h1 class="h1">Abzeichen für {{ rowingbadge_year }}</h1>
<div class="text-black dark:text-white">
<table id="basic">
<thead>
<tr>
<th>Name</th>
<th>Erster Log</th>
<th>Letzter Log</th>
<th>Gesamt-KM</th>
<th>Äquatorpreis (ÄP)</th>
<th>
ÄP diese
<br>
Saison bekommen
</th>
<th>
Fahrtenabzeichen (FA)
<br>
geschafft
</th>
<th>FA - KM</th>
<th>FA - fehlende KM</th>
<th>Eintagesausfahrten</th>
<th>Mehrtagesausfahrten</th>
</tr>
</thead>
<tbody>
{% for person in people %}
{% set user = person.0 %}
{% set achievement = person.1 %}
<tr>
<td>{{ user.name }}</td>
<td>
{% if achievement.year_first_mentioned %}{{ achievement.year_first_mentioned }}{% endif %}
</td>
<td>
{% if achievement.year_last_mentioned %}{{ achievement.year_last_mentioned }}{% endif %}
</td>
<td>{{ achievement.all_time_km }}</td>
<td>{{ achievement.curr_equatorprice_name }}</td>
<td>
{% if achievement.new_equatorprice_this_season %}
🎉
{% else %}
-
{% endif %}
</td>
{% if achievement.rowingbadge %}
{% set badge = achievement.rowingbadge %}
<td>
{% if badge.achieved %}
ja
{% else %}
nein
{% endif %}
</td>
<td>{{ badge.rowed_km }} / {{ badge.required_km }}</td>
<td>{{ badge.missing_km }}</td>
<td>
<details>
<summary>
> {{ badge.single_day_trips_required_distance }} km: {{ badge.single_day_trips_over_required_distance | length }} / 2
</summary>
{% for log in badge.single_day_trips_over_required_distance %}
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, hide_type=true) }}
{% endfor %}
</details>
</td>
<td>
<details>
<summary>
> {{ badge.multi_day_trips_required_distance }} km: {{ badge.multi_day_trips_over_required_distance | length }} / 1
</summary>
{% for log in badge.multi_day_trips_over_required_distance %}
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, hide_type=true) }}
{% endfor %}
</details>
</td>
{% else %}
<td>Geb.datum fehlt 👻</td>
<td>Geb.datum fehlt 👻</td>
<td>Geb.datum fehlt 👻</td>
<td>Geb.datum fehlt 👻</td>
<td>Geb.datum fehlt 👻</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script src="/public/jstable.min.js"></script>
<script src="/public/table.js"></script>
{% endblock content %}

View File

@ -8,11 +8,20 @@
role="alert">
<h2 class="h2">Angemeldete Personen: {{ schnupperanten | length }}</h2>
<div class="text-sm p-3">
<ol class="ms-2" style="list-style: number;">
<ol class="list-decimal">
{% 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 class="py-1 ml-4">
<span class="flex items-center justify-between">
<span>
<span class="status-damage status-damage-{% if "paid" in user.roles %}none {% else %}locked {% endif %}"></span>&nbsp;{{ user.name }} ({{ user.mail }}
{%- if user.notes %} | {{ user.notes }}
{% endif -%}
)
</span>
<a class="btn btn-primary"
href="/admin/user/move/schnupperant/{{ user.id }}/to/scheckbuch"
onclick="return confirm('Willst du wirklich ein Scheckbuch erstellen? Die Person erhält ein Mail mit allen Infos.')">Zu Scheckbuch umwandeln</a>
</span>
</li>
{% endfor %}
</ol>

View File

@ -1,43 +1,44 @@
{% 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="max-w-screen-lg w-full">
<h1 class="h1">
Gebühren
</h1>
<div class="search-wrapper">
<label for="name" class="sr-only">Suche</label>
<input type="search"
name="name"
id="filter-js"
class="search-bar"
placeholder="Suchen nach Name" />
placeholder="Suchen nach Namen..." />
</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>
<div id="filter-result-js" class="search-result"></div>
<div class="border-r border-l border-gray-200 dark:border-primary-600">
{% for fee in fees | sort(attribute="name") %}
<div {% if fee.paid %}style="background-color: green;"{% endif %}
<div class="border-t border-gray-200 dark:border-primary-600 {% if fee.paid %}bg-[#15803d] text-white {% else %} bg-white {% endif %} dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1 "
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 class="grid md:grid-cols-3 gap-3 w-full py-3">
<div>
<strong>{{ fee.name }}</strong>
<span class="block">{{ fee.sum_in_cents / 100 }}€</span>
</div>
<div style="width: 100%">{{ fee.sum_in_cents / 100 }}€:</div>
<div style="width: 100%">
<div>
{% 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>
{% if "admin" in loggedin_user.roles or "kassier" in loggedin_user.roles %}
<div class="text-end"><a href="/admin/user/fees/paid?{{ fee.user_ids }}" class="btn btn-primary">Zahlungsstatus ändern</a></div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock content %}

View File

@ -36,37 +36,53 @@
placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" />
</div>
<!-- END filterBar -->
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
<div id="filter-result-js"
class="text-primary-950 dark:text-white text-right"></div>
{% for user in users %}
<div data-filterable="true"
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %} ">
<div id="filter-result-js" class="search-result"></div>
{% for user in users %}
<div data-filterable="true"
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %}"
class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
<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 "admin" in loggedin_user.roles and user.mail %}
<form action="/admin/user"
method="post"
enctype="multipart/form-data"
class="inline">
&bullet; <a class="font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/send-welcome-mail"
onclick="return confirm('Willst du wirklich das Willkommensmail an {{ user.name }} ausschicken?');">Willkommensmail verschicken</a>
</form>
{% endif %}
{% if user.last_access %}&bullet; ⏳&nbsp;{{ user.last_access | date }}{% endif %}
</span>
<small class="block text-gray-600 dark:text-gray-100">
{% for role in user.roles -%}
{{ role }}
{%- if not loop.last %},
{% endif -%}
{% endfor %}
</small>
</span>
</summary>
<form action="/admin/user"
method="post"
enctype="multipart/form-data"
class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
<div class="w-full grid gap-3">
class="w-full mt-2">
{% if user.pw %}
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/reset-pw"
onclick="return confirm('Willst du wirklich das Passwort zurücksetzen?');">Passwort zurücksetzen</a>
{% endif %}
<div class="w-full grid gap-3 mt-3">
<input type="hidden" name="id" value="{{ user.id }}" />
<div class="font-bold mb-1 text-black dark:text-white">
{{ user.name }}
{% if user.last_access %}
(last access:
{{ user.last_access | date }})
{% endif %}
{% if user.pw %}
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a>
{% endif %}
{% if not user.last_access and "admin" in loggedin_user.roles %}
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/send-welcome-mail">Willkommensmail verschicken</a>
{% endif %}
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
{% for role in roles %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
{% endfor %}
<hr class="sm:col-span-2 lg:col-span-4 my-3" />
{% if user.membership_pdf %}
<a href="/admin/user/{{ user.id }}/membership"
class="text-black dark:text-white">Beitrittserklärung herunterladen</a>
@ -100,8 +116,8 @@
</div>
{% endif %}
</form>
</div>
{% endfor %}
</div>
</details>
</div>
{% endfor %}
</div>
{% endblock content %}

View File

@ -4,6 +4,34 @@
{% 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>
<form action="/admin/user/new/scheckbuch"
method="post"
class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between">
<div class="w-full">
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neues Scheckbuch hinzufügen</h2>
<div class="grid md:grid-cols-3">
<div>
<label for="name" class="sr-only">Name</label>
<input type="text"
name="name"
class="relative block rounded-md border-0 py-1.5 px-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6 mb-2 md:mb-0"
placeholder="Name" />
</div>
<div>
<label for="name" class="sr-only">Mail</label>
<input type="mail"
name="mail"
class="relative block rounded-md border-0 py-1.5 px-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6 mb-2 md:mb-0"
placeholder="Mail" />
</div>
</div>
</div>
<div class="text-right">
<input value="Hinzufügen"
type="submit"
class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
</div>
</form>
<!-- START filterBar -->
<div class="search-wrapper">
<label for="name" class="sr-only">Suche</label>
@ -31,7 +59,7 @@
<li>{{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}</li>
{% endfor %}
</div>
{% if "admin" in loggedin_user.roles %}
{% if "admin" in loggedin_user.roles or "kassier" in loggedin_user.roles %}
<a href="/admin/user/fees/paid?user_ids[]={{ user.id }}">Zahlungsstatus ändern</a>
{% endif %}
</div>

View File

@ -10,7 +10,7 @@
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', required=true, min='0') }}
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='tripdetails.max_people', type='number', required=true, min='0') }}
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='tripdetails.allow_guests') }}
{{ macros::checkbox(label='Immer anzeigen', name='tripdetails.always_show') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show') }}
{{ macros::input(label='Anmerkungen', name='tripdetails.notes', type='input') }}
{{ macros::select(label='Typ', data=trip_types, name='tripdetails.trip_type', default='Reguläre Ausfahrt') }}
<input value="Erstellen" class="w-full btn btn-primary" type="submit" />

View File

@ -5,7 +5,6 @@
{{ macros::input(label='Startzeit (zB "10:00")', name='planned_starting_time', type='time', required=true) }}
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }}
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='allow_guests') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show') }}
{{ macros::input(label='Anmerkungen', name='notes', type='input') }}
{{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }}
<input value="Erstellen" class="w-full btn btn-primary" type="submit" />

View File

@ -29,7 +29,7 @@
Folgende Daten werden verarbeitet:
<ul>
<li>
Server-Log Files: IP-Adresse, Adresse der besuchten Seite, Browseragent, Datum und Uhrzeit. Wir nutzen diese Daten nicht und geben Sie in der Regel nicht weiter, können jedoch nicht ausschließen, dass diese Daten beim Vorliegen von rechtswidrigem Verhalten eingesehen werden.
Server-Log Files: IP-Adresse, Adresse der besuchten Seite, Browseragent, Datum und Uhrzeit. Wir nutzen diese Daten nicht und geben sie in der Regel nicht weiter, können jedoch nicht ausschließen, dass diese Daten beim Vorliegen von rechtswidrigem Verhalten eingesehen werden.
</li>
<li>
Cookie: Diese Website verwendet nur einen Cookie (loggedin_user), der verschlüsselte Informationen über den Login-Status speichert. Weitere Cookies werden nicht verwendet.

View File

@ -5,7 +5,7 @@
{% macro show_boats() %}
{% for cat, grouped_boats in boats | group_by(attribute="cat") %}
<details>
<summary class="font-bold cursor-pointer text-primary-900 dark:text-white border-t p-3 hover:bg-gray-100 dark:hover:bg-primary-950">
<summary class="font-bold cursor-pointer text-primary-900 dark:text-white {% if not loop.first %}border-t{% endif %} p-3 hover:bg-gray-100 dark:hover:bg-primary-950">
<span>{{ cat }}</span>
<small class="text-gray-500 dark:text-gray-100">({{ grouped_boats | length }})</small>
</summary>
@ -36,7 +36,7 @@
<div class="col-span-4 md:col-span-1">
<div class="text-sm text-gray-600 dark:text-gray-100">Bootssteuerung</div>
<div class="h-10 flex items-center">
{{ macros::checkbox(label='handgesteuert', name='shipmaster_only_steering', disabled=true) }}
{{ macros::checkbox(label='handgesteuert', name='shipmaster_only_steering', readonly=true) }}
</div>
</div>
{{ log::rower_select(id="newrower", selected=[], class="col-span-4", init=true) }}
@ -53,9 +53,13 @@
id="destination"
name="destination"
value=""
data-relation="distance_in_km" />
data-relation="distance_in_km"
autocomplete="off" />
<datalist id="destinations">
{% for distance in distances %}<option value="{{ distance.0 }}" distance="{{ distance.1 }}" />{% endfor %}
{% for distance in distances %}
<option value="{{ distance.destination }}"
distance="{{ distance.distance_in_km }}" />
{% endfor %}
</datalist>
</div>
<div class="relative col-span-2">
@ -169,83 +173,114 @@
</div>
</div>
{% endmacro show %}
{% macro show_old(log, state, allowed_to_close=false, index) %}
{% macro show_old(log, state, allowed_to_close=false, allowed_to_edit=false, index, hide_type=false) %}
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative"
data-filterable="true"
data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}">
{% if log.logtype %}
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
{% if log.logtype == 1 %}
Wanderfahrt
{% else %}
{% if log.logtype == 2 %}
Regatta
{% else %}
{{ log.logtype }}
{% endif %}
{% endif %}
</div>
{% endif %}
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
{% if log.shipmaster_only_steering %}
- handgesteuert
{%- endif -%}
)</small>
<small class="block text-gray-600 dark:text-gray-100">
{% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %}
{{ log.departure | date(format='%d.%m.%Y') }}
({{ log.departure | date(format='%H:%M') }}
-
{{ log.arrival | date(format='%H:%M') }})
{% else %}
{{ log.departure | date(format='%d.%m.%Y (%H:%M)') }}
{% if state == "completed" %}
-
{{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
{% endif %}
{% endif %}
</small>
{% set amount_rowers = log.rowers | length %}
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
{% if allowed_to_close and state == "on_water" %}
{{ log::home(log=log) }}
{% else %}
<div class="text-black dark:text-white">
{{ log.destination }}
{% if state == "completed" %}
<small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }}
km)</small>
{% endif %}
{% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %}
</div>
{% if amount_guests > 0 or log.rowers | length > 0 %}
{% if not log.boat.amount_seats == 1 %}
<div class="text-sm text-gray-600 dark:text-gray-100">
Ruderer:
{% for rower in log.rowers -%}
{{ rower.name }}
{%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %}
(Steuerperson){%- endif -%}
{%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %}
{% endfor -%}
{% if amount_guests > 0 and not log.boat.external %}
Gäste
<small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>:
{{ amount_guests }}
{% endif %}
</div>
<details>
<summary style="list-style: none;">
{% if log.logtype and not hide_type %}
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
{% if log.logtype == 1 %}
Wanderfahrt
{% else %}
{% if log.logtype == 2 %}
Regatta
{% else %}
{{ log.logtype }}
{% endif %}
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
{% if log.shipmaster_only_steering %}
- handgesteuert
{%- endif -%}
)</small>
<small class="block text-gray-600 dark:text-gray-100">
{% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %}
{{ log.departure | date(format='%d.%m.%Y') }}
({{ log.departure | date(format='%H:%M') }}
-
{{ log.arrival | date(format='%H:%M') }})
{% else %}
{{ log.departure | date(format='%d.%m.%Y (%H:%M)') }}
{% if state == "completed" %}
-
{{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
{% endif %}
{% endif %}
</small>
{% set amount_rowers = log.rowers | length %}
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
{% if allowed_to_close and state == "on_water" %}
{{ log::home(log=log) }}
{% else %}
<div class="text-black dark:text-white">
{{ log.destination }}
{% if state == "completed" %}
<small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }}
km)</small>
{% endif %}
{% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %}
</div>
{% if amount_guests > 0 or log.rowers | length > 0 %}
{% if not log.boat.amount_seats == 1 %}
<div class="text-sm text-gray-600 dark:text-gray-100">
Ruderer:
{% for rower in log.rowers -%}
{{ rower.name }}
{%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %}
(Steuerperson){%- endif -%}
{%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %}
{% endfor -%}
{% if amount_guests > 0 and not log.boat.external %}
Gäste
<small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>:
{{ amount_guests }}
{% endif %}
</div>
{% endif %}
{% endif %}
{% endif %}
</div>
</summary>
{% if allowed_to_edit %}
<form action="/log/update" method="post">
<input type="hidden" name="id" value="{{ log.id }}" />
<input type="hidden" name="boat_id" value="{{ log.boat_id }}" />
<input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" />
<input type="hidden"
name="steering_person"
value="{{ log.steering_person }}" />
<input type="hidden"
name="shipmaster_only_steering"
value="{{ log.shipmaster_only_steering }}" />
<input type="datetime-local" name="departure" value="{{ log.departure }}" />
<input type="datetime-local" name="arrival" value="{{ log.arrival }}" />
<input type="hidden" name="destination" value="{{ log.destination }}" />
<input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" />
<input type="hidden" name="comments" value="{{ log.comments }}" />
<input type="hidden" name="logtype" value="{{ log.logtype }}" />
<input type="submit" value="Updaten" />
</form>
<a href="/log/{{ log.id }}/delete"
class="w-28 btn btn-alert"
onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');">
{% include "includes/delete-icon" %}
Löschen
</a>
{% endif %}
</details>
</div>
{% endmacro show_old %}
{% macro home(log) %}
<form class="grid grid-cols-1 gap-3"
action="/log/{{ log.id }}"
method="post">
method="post"
onsubmit="var distance = parseFloat(document.getElementById('distance_in_km{{ log.id }}js').value); var logtype = document.getElementById('logtype{{ log.id }}js').value; if (distance > 50 && (!logtype || logtype === 'Normal')) { return confirm('Die eingegebene Distanz beträgt mehr als 50 km und es wurde kein Typ (Wanderfahrt, Regatta, ...) ausgewählt. Wenn es eine Wanderfahrt war, stell dies bitte unter \'Details ändern\' ein. Möchtest du das NICHT tun und den Eintrag OHNE Typ speichern?'); } return true;">
{{ macros::input(label='Ankunftszeit', name='arrival', type='datetime-local', required=true, class="change-id-js rounded-md current-date-time") }}
<div>
<label for="destination" class="text-sm text-gray-600 dark:text-gray-100">Ziel</label>

View File

@ -1,10 +1,50 @@
{% macro plannedtrips() %}
{% if planned_trips %}
<script>
function setChoiceByLabel(choicesInstance, label) {
const choices = choicesInstance.config.choices;
const normalizedLabel = label.trim().replace(/\n/g, '');
const choice = choices.find(c => {
const choiceLabel = c.label.trim().replace(/\n/g, '');
return choiceLabel === normalizedLabel;
});
if (choice) {
choicesInstance.setChoiceByValue(choice.value);
} else {
console.warn(`Choice with label "${normalizedLabel}" not found`);
}
}
</script>
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2 mt-3">
<h2 class="h2">Heute geplante Ausfahrten</h2>
<div class="grid grid-cols-1 gap-3 w-full">
{% for planned_trip in planned_trips | sort(attribute='planned_starting_time') %}
<div class="pt-2 px-3 {% if not loop.first %}border-t{% endif %} text-primary-900 dark:text-white flex justify-between items-center">
<strong class="block">
{% set amount_members = planned_trip.rower | length + 1 %}
{{ planned_trip.cox_name }} ({{ amount_members }}&nbsp;Person{{ amount_members | pluralize(singular="", plural="en") }})
<small class="block">{{ planned_trip.planned_starting_time }}</small>
</strong>
<button class="btn btn-primary ml-3"
onclick="choiceObjects['newrower'].removeActiveItems(-2);choiceObjects['newrower'].setChoiceByValue('{{ planned_trip.cox_id }}'); {% for rower in planned_trip.rower %}setChoiceByLabel(choiceObjects['newrower'], '{{ rower.name }}');{% endfor %}window.scrollTo(0,0); ">
👥
</button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endmacro plannedtrips %}
{% macro boatreservation() %}
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2 mt-3">
<h2 class="h2">Reservierungen ({{ reservations | length }})</h2>
<div class="grid grid-cols-1 gap-3 mb-3 w-full">
{% for _, reservations_for_event in reservations %}
{% set reservation = reservations_for_event[0] %}
<div class="pt-2 px-3 border-t text-primary-900 dark:text-white">
<div class="pt-2 px-3 {% if not loop.first %}border-t{% endif %} text-primary-900 dark:text-white">
<strong class="block">
{{ reservation.start_date | date(format="%d.%m.%Y") }}
{% if reservation.end_date != reservation.start_date %}
@ -137,7 +177,7 @@
{% if readonly %}readonly{% endif %}>
</div>
{% endmacro input %}
{% macro checkbox(label, name, id='', checked=false, class='', disabled=false) %}
{% macro checkbox(label, name, id='', checked=false, class='', disabled=false, readonly=false) %}
<label for="{{ name }}{{ id }}"
class="flex items-center cursor-pointer text-black dark:text-white hover:text-gray-900 dark:hover:text-gray-100 {{ class }}">
<input type="checkbox"
@ -145,6 +185,7 @@
name="{{ name }}"
{% if checked %}checked{% endif %}
{% if disabled %}disabled{% endif %}
{% if readonly %}readonly="readonly"{% endif %}
class="h-4 w-4 accent-primary-600 dark:accent-primary-200 mr-2" />
{{ label }}
</label>

View File

@ -33,6 +33,11 @@
<div class="mt-1">{{ notification.message | safe }}</div>
</div>
<div>
{% if notification.link %}
<a href="{{ notification.link }}" class="inline-block">
<button class="btn btn-primary" type="button">🔗</button>
</a>
{% endif %}
{% if not notification.read_at %}
<a href="/notification/{{ notification.id }}/read" class="inline-block">
<button class="btn btn-primary" type="button">
@ -56,6 +61,11 @@
<strong>{{ notification.category }}</strong> &bullet; {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }}
</small>
<div class="mt-1">{{ notification.message | safe }}</div>
{% if notification.link %}
<a href="{{ notification.link }}" class="inline-block">
<button class="btn btn-primary" type="button">🔗</button>
</a>
{% endif %}
</div>
{% endif %}
{% endfor %}
@ -64,6 +74,176 @@
</div>
{% endif %}
{% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
<dialog id="call-for-action"
class="max-w-screen-sm dark:bg-primary-600 dark:text-white rounded-md"
onclick="document.getElementById('call-for-action').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('call-for-action').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<p>
Du hast Ideen für sinnvolle neue Funktionen für diese Ruderapp? Melde dich bei Philipp, Marie oder <a href="mailto:it@rudernlinz.at" class="underline">it@rudernlinz.at</a>.
</p>
<p class="mt-3">
Wenn du darüber hinaus Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird (😀), dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.
</p>
</div>
</div>
</dialog>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">
Deine Ruderkarriere
<span class="text-xl"
onclick="document.getElementById('call-for-action').showModal()">💡</span>
</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
<details>
<summary>
<h3 class="inline">
<span class="text-xl">
{% if achievements.rowingbadge and achievements.rowingbadge.achieved %}
&#127881;
{% else %}
📋
{% endif %}
</span>
Fahrtenabzeichen
{% if achievements.rowingbadge %}{{ achievements.rowingbadge.year }}{% endif %}
<span><a href="http://www.rudern.at/OFFICE/Downloads/Ausschreibungen/2022/Wanderfahrten//Fahrtenabzeichen%20%C3%84quatorpreis%20und%20Danubius%202022.pdf"
target="_blank"
class="w-6 h-6 inline-flex align-center justify-center rounded-full bg-primary-500 ml-2 text-white">?</a></span>
</h3>
</summary>
{% if achievements.rowingbadge %}
{% set badge = achievements.rowingbadge %}
<div class="mb-3">{{ badge.category }}</div>
<label for="rowingbadge" class="label">Kilometer ({{ badge.rowed_km }} / {{ badge.required_km }} km)</label>
<progress id="rowingbadge"
class="w-full block my-3"
value="{{ badge.rowed_km }}"
max="{{ badge.required_km }}"></progress>
<h4 class="font-bold mt-4">Wanderfahrten</h4>
<div>Nur eine der folgenden Optionen muss erreicht werden:</div>
<ol class="list-decimal ml-4 my-3">
<li>
{% if badge.multi_day_trips_over_required_distance | length >= 1 %}
&#9989;
{% else %}
&#10060;
{% endif %}
1 mehrtägige Wanderfahrt > {{ badge.multi_day_trips_required_distance }} km
</li>
<li>
{% if badge.single_day_trips_over_required_distance | length >= 2 %}
&#9989;
{% else %}
&#10060;
{% endif %}
2 eintägige Wanderfahrten > {{ badge.single_day_trips_required_distance }} km
</li>
</ol>
<details>
<summary>Details zu den Wanderfahrten</summary>
<div class="mt-3">
{% for log in badge.single_day_trips_over_required_distance %}
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}
{% endfor %}
{% for log in badge.multi_day_trips_over_required_distance %}
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}
{% endfor %}
</div>
</details>
{% else %}
<div class="mt-3">
Wir haben leider kein Geburtsdatum von dir und können dir leider deinen heurigen Status für das Fahrtenabzeichen nicht anzeigen. Wenn du dein Geburtsdatum an <a href="mailto:it@rudernlinz.at" class="underline">it@rudernlinz.at</a> schreibst, lässt sich das ändern :-)
</div>
{% endif %}
</details>
</div>
<div class="py-3">
<details>
<summary>
<h3 class="mb-3 inline">
{% set price = achievements.equatorprice %}
<span class="text-xl">
{% if achievements.curr_equatorprice_name == "-" %}
📋
{% elif achievements.curr_equatorprice_name == "Bronze" %}
🥉
{% elif achievements.curr_equatorprice_name == "Silber" %}
🥈
{% elif achievements.curr_equatorprice_name == "Gold" %}
🥇
{% elif achievements.curr_equatorprice_name == "Diamant" %}
💍
{% endif %}
</span>
Äquatorpreis
<span><a href="http://www.rudern.at/OFFICE/Downloads/Ausschreibungen/2022/Wanderfahrten//Fahrtenabzeichen%20%C3%84quatorpreis%20und%20Danubius%202022.pdf"
target="_blank"
class="w-6 h-6 inline-flex align-center justify-center rounded-full bg-primary-500 ml-2 text-white">?</a></span>
</h3>
</summary>
<div class="mt-3">
{% if price.level == "DONE" %}
Gratuliere, du hast alles in deinem Rudererleben erreicht, was es (beim Äquatorpreis) zu erreichen gibt.
{% else %}
<label for="equatorprice" class="label">{{ price.desc }} ({{ price.rowed_km }} / {{ price.required_km }} km)</label>
<progress id="equatorprice"
class="w-full block my-3"
value="{{ price.rowed_km }}"
max="{{ price.required_km }}"></progress>
<details>
<summary>Details</summary>
Du bist insgesamt {{ price.rowed_km }} km gerudert. Um den Äquatorpreis in {{ price.desc }} zu erhalten, benötigst du noch {{ price.missing_km }} km um die notwendigen {{ price.required_km }} km zu erreichen.
</details>
{% endif %}
</div>
</details>
</div>
<div class="py-3">
<p>
<details>
<summary>
<span class="text-xl">&nbsp;&#128197;&nbsp;</span>&nbsp;Kalender
</summary>
<p class="mt-3">
Du möchtest immer up-to-date mit den Events und Ausfahrten bleiben? Wir bieten 3 verschiedene Arten von Kalender an:
</p>
<ol class="list-decimal ml-5 my-3">
<li>
<strong>Alle Events und Ausfahrten</strong>, zu denen du dich angemeldet hast: <a class="underline break-all"
href="https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}">https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}</a>
<br />
<small>Dieser Link enthält einen zufällig generierten Teil, damit nur du (und jene, denen du diesen Link weitergibst) Zugang zu diesen Daten hast.</small>
</li>
<li>
<strong>Allgemeiner Kalender</strong>, zB save-the-dates (Wanderfahrten, ...): <a href="https://rudernlinz.at/cal" class="break-all underline">https://rudernlinz.at/cal</a>
</li>
<li>
<strong>Alle Events</strong>: <a class="break-all underline" href="https://app.rudernlinz.at/cal">https://app.rudernlinz.at/cal</a>
<br />
<small>Beachte, dass dieser Kalender keine Ausfahrten enthält, die von einzelnen Steuerpersonen augeschrieben werden. Dieser Kalender wird zB auf <a href="https://rudernlinz.at/termine" class="underline">https://rudernlinz.at/termine</a> verwendet und wir möchten keine persönlichen Daten (Namen etc.) leaken.</small>
</li>
</ol>
Du kannst die Kalender einfach in deinen Kalender als "externen Kalender" synchronisieren. Die genauen Schritte hängen von deiner verwendeten Software ab.
</details>
</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Aktives Vereinsmitglied</h2>
@ -155,6 +335,17 @@
<a href="/board/boathouse"
class="block w-100 py-2 hover:text-primary-600">Bootshaus</a>
</li>
<li class="py-1">
<a href="/admin/mail" class="block w-100 py-2 hover:text-primary-600">Mail ausschicken</a>
</li>
<li class="py-1">
<a href="/admin/notification"
class="block w-100 py-2 hover:text-primary-600">Nachricht ausschreiben</a>
</li>
<li class="py-1">
<a href="/board/achievement"
class="block w-100 py-2 hover:text-primary-600">Abzeichen</a>
</li>
</ul>
</div>
{% endif %}
@ -169,19 +360,12 @@
<li class="py-1">
<a href="/admin/user" class="block w-100 py-2 hover:text-primary-600">User</a>
</li>
<li class="py-1">
<a href="/admin/mail" class="block w-100 py-2 hover:text-primary-600">Mail</a>
</li>
<li class="py-1">
<a href="/admin/rss" class="block w-100 py-2 hover:text-primary-600">Logs</a>
</li>
<li class="py-1">
<a href="/admin/list" class="block w-100 py-2 hover:text-primary-600">Fingerabdruck-Liste überprüfen</a>
</li>
<li class="py-1">
<a href="/admin/notification"
class="block w-100 py-2 hover:text-primary-600">Nachricht ausschreiben</a>
</li>
</ul>
</div>
{% endif %}

View File

@ -53,6 +53,7 @@
</div>
</div>
{{ macros::boatreservation() }}
{{ macros::plannedtrips() }}
</div>
</div>
</div>

View File

@ -23,7 +23,15 @@
placeholder="Suchen nach Bootsname oder Ruderer...">
</div>
<div id="filter-result-js" class="search-result"></div>
{% for log in logs %}{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}{% endfor %}
{% for log in logs %}
{% set_global allowed_to_edit = false %}
{% if loggedin_user %}
{% if "Vorstand" in loggedin_user.roles %}
{% set_global allowed_to_edit = true %}
{% endif %}
{% endif %}
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=allowed_to_edit) }}
{% endfor %}
</div>
</div>
<script>

View File

@ -35,6 +35,7 @@
{% endif %}
</div>
{{ macros::boatreservation() }}
{{ macros::plannedtrips() }}
</div>
</div>
</div>

View File

@ -18,8 +18,8 @@
<script type="text/javascript">
var sepaqr = new sepaQR({
benefName: 'ASKÖ Ruderverein Donau Linz',
benefBIC: 'BKAUATWWXXX',
benefAccNr: 'AT131200080413001200',
benefBIC: 'ASPKAT2LXXX',
benefAccNr: 'AT582032032100729256',
amountEuro: {{ fee.sum_in_cents/100 }},
remittanceInf: 'Vereinsgebühren {{ fee.name }}',
});
@ -44,13 +44,13 @@
</ul>
</small>
{% endif %}
Bitte auf folgendes Konto überweisen: IBAN AT13 1200 0804 1300 1200. Alternativ kannst du auch mit deiner Bankapp den QR Code scannen, damit sollten alle Daten vorausgefüllt sein.
Bitte auf folgendes Konto überweisen: IBAN AT58 2032 0321 0072 9256. Alternativ kannst du auch mit deiner Bankapp den QR Code scannen, damit sollten alle Daten vorausgefüllt sein.
<br />
Falls die Berechnung nicht stimmt (korrekte Preise findest du <a href="https://rudernlinz.at/unser-verein/gebuhren/"
target="_blank"
rel="noopener noreferrer">hier</a>) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.
<br />
<small>Wir aktualisieren den Ruderassistent unregelmäßig mit unserem Bankkonto. Falls du schon bezahlt hast, kannst du diese Nachricht getrost ignorieren :^)</small>
<small><a href="https://rudernlinz.at/unser-verein/vorstand/" target="_blank">Unsere Kassiere</a> aktualisieren den Ruderassistent unregelmäßig mit unserem Bankkonto. Falls du schon bezahlt hast, kannst du diese Nachricht getrost ignorieren. Wenn du schon vor "einigen Wochen" bezahlt hast bitte bei kassier@rudernlinz.at nachfragen :^)</small>
</div>
</div>
</div>
@ -67,7 +67,8 @@
{% endif %}
{% endfor %}
{% endif %}
<div class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js"
<div id="{{ day.day| date(format="%Y-%m-%d") }}"
class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js"
style="min-height: 10rem"
data-trips="{{ amount_trips }}"
data-month="{{ day.day| date(format='%m') }}"
@ -99,7 +100,10 @@
style="order: {{ event.planned_starting_time | replace(from=":", to="") }}">
<div class="flex justify-between items-center">
<div class="mr-1">
{% if event.max_people == 0 %}
{% if event.always_show and not day.regular_sees_this_day %}
<span title="Du siehst diese Ausfahrt schon, obwohl sie mehr als {{ amount_days_to_show_trips_ahead }} Tage in der Zukunft liegt. Du Magier!">🔮</span>
{% endif -%}
{%- if event.max_people == 0 %}
<strong class="text-[#f43f5e]">&#9888; Absage
{{ event.planned_starting_time }}
Uhr
@ -273,6 +277,9 @@
data-coxneeded="false">
<div class="flex justify-between items-center">
<div class="mr-1">
{% if trip.always_show and not day.regular_sees_this_day %}
<span title="Du siehst diese Ausfahrt schon, obwohl sie mehr als {{ amount_days_to_show_trips_ahead }} Tage in der Zukunft liegt. Du Magier!">🔮</span>
{% endif -%}
{% if trip.max_people == 0 %}
<strong class="text-[#f43f5e]">&#9888;
{{ trip.planned_starting_time }}
@ -326,7 +333,7 @@
<div id="trip{{ trip.trip_details_id }}">
{% if trip.max_people == 0 %}
{# --- border-[#f43f5e] bg-[#f43f5e] --- #}
{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage') }}
{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
{% else %}
{% set amount_cur_rower = trip.rower | length %}
{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
@ -346,7 +353,6 @@
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3">
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min=trip.rower | length) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=trip.id,checked=trip.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }}
<input value="Speichern" class="btn btn-primary" type="submit" />
@ -369,7 +375,6 @@
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid">
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
{{ macros::input(label='', name='always_show', type='hidden', value=trip.always_show) }}
{{ macros::input(label='', name='is_locked', type='hidden', value=trip.is_locked) }}
{{ macros::input(label='', name='trip_type', type='hidden', value=trip.trip_type_id) }}
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
@ -379,6 +384,32 @@
{% endif %}
{% endif %}
{# --- END Edit Form --- #}
{# --- START Admin Form --- #}
{% if allowed_to_update_always_show_trip %}
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Admin-Modus</h3>
{% if not day.regular_sees_this_day %}
<form action="/cox/trip/{{ trip.id }}/toggle-always-show"
method="get"
class="grid gap-3">
{% if not trip.always_show %}
<small>Diese Ausfahrt sehen aktuell nur Steuerleute (und Admins). {{ amount_days_to_show_trips_ahead }} Tage vorher sehen sie dann alle.</small>
{% else %}
<small>Diese Ausfahrt sehen alle Mitglieder.</small>
{% endif %}
<input value="{% if trip.always_show %}Ausfahrt nur Steuerleute (und Admins) anzeigen{% else %}Ausfahrt allen anzeigen{% endif %}"
class="btn btn-primary"
type="submit" />
</form>
{% endif %}
<a href="/cox/remove/trip/{{ trip.id }}"
class="inline-block btn btn-alert mt-5 w-full">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{# --- END Admin Form --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
@ -391,15 +422,15 @@
</div>
{# --- START Add Buttons --- #}
{% if "manage_events" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "manage_events" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
<div class="grid {% if "manage_events" in loggedin_user.roles and "cox" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
{% if "manage_events" in loggedin_user.roles %}
<a href="#"
data-sidebar="true"
data-trigger="sidebar"
data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen"
data-day="{{ day.day }}"
data-body="#addEventForm"
class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold">
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 text-sm font-semibold
{% if "cox" in loggedin_user.roles %}
rounded-bl-md
{% else %}
rounded-b-md
{% endif %}
">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">{% include "includes/plus-icon" %}</span>
Event
</a>

View File

@ -9,6 +9,7 @@
<thead>
<tr>
<th>Name</th>
<th>Art</th>
<th>Eigentümer</th>
<th>Ort</th>
{% for year in stat.pot_years | sort | reverse %}<th>{{ year }}</th>{% endfor %}
@ -18,6 +19,7 @@
{% for boat in stat.boats %}
<tr>
<td>{{ boat.name }}</td>
<td>{{ boat.cat }}</td>
<td>{{ boat.owner }}</td>
<td>{{ boat.location }}</td>
{% for year in stat.pot_years | sort | reverse %}

View File

@ -51,7 +51,7 @@
data-filter="{{ reservation.user_applicant.name }} {{ reservation.trailer.name }}"
class="w-full border-t bg-white dark:bg-primary-900 text-black dark:text-white p-3">
<div class="w-full">
<strong>Boot:</strong>
<strong>Hänger:</strong>
{{ reservation.trailer.name }}
<br />
<strong>Reservierung:</strong>

156
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);
```