160 Commits

Author SHA1 Message Date
2159696112 fix migration
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) Has been skipped
2024-06-16 20:00:44 +02:00
39bde35864 Merge pull request 'add wifi pw in welcome mail' (#593) from add-wifi-pw-new-members 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 6m45s
Reviewed-on: #593
2024-06-10 22:08:18 +02:00
5f301324ee add wifi pw in welcome mail
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-10 22:07:14 +02:00
9558965e8f Merge pull request 'require necessary fields' (#591) from require-necessary-fields 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 5m48s
Reviewed-on: #591
2024-06-10 20:59:43 +02:00
5f4d8982a8 require necessary fields
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-06-10 20:58:42 +02:00
0e2ef9e256 Merge pull request 'add trailer reservation funcitonality; Fixes #443' (#588) from trailer-reservation into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m3s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m7s
Reviewed-on: #588
2024-06-10 20:00:25 +02:00
1dc91f4f28 merged :-)
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-06-10 19:59:39 +02:00
f56da43723 fix copied stuff
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 19:48:16 +02:00
9dc1ec6fa0 add trailer reservation funcitonality; Fixes #443
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-06-10 19:43:59 +02:00
c9b67f5790 Merge pull request 'add delete trip button in edit window; Fixes #449' (#586) from delte-trip-btn into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m53s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m5s
Reviewed-on: #586
2024-06-10 19:08:22 +02:00
16687e39ab remove unused attribute
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-10 19:07:55 +02:00
957c474389 add delete trip button in edit window; Fixes #449
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-06-10 19:07:05 +02:00
f7aed68423 Merge pull request 'show if a user has < 30 km to thousand km trip, Fixes #551' (#582) from thousand-km-trips into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m59s
Reviewed-on: #582
2024-06-10 15:50:31 +02:00
01637d0800 Merge pull request 'use proper hydro license; add fluctuation' (#584) from proper-hydro 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: #584
2024-06-10 15:49:51 +02:00
0a77011170 use proper hydro license; add fluctuation
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-10 15:48:49 +02:00
dea0c65da3 fix ci + add test
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-10 15:12:23 +02:00
31bf38f112 show if a user has < 30 km to thousand km trip, Fixes #551
Some checks failed
CI/CD Pipeline / test (push) Failing after 7m14s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-10 14:55:01 +02:00
6b29907596 Merge pull request 'fix boat select' (#580) from fix-boat-select into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m2s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m34s
Reviewed-on: #580
2024-06-10 11:01:47 +02:00
7b17c30ce2 fix boat select
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-06-10 11:00:54 +02:00
ec4068e499 Merge pull request 'cleaner cal' (#578) from cleaner-cal into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m47s
Reviewed-on: #578
2024-06-06 17:22:14 +02:00
fca19745f8 even nicer cal entries
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-06 17:21:55 +02:00
bb48ddb3de fix ci
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-06-06 17:17:40 +02:00
34b098fa2a cleaner cal
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-06-06 17:16:47 +02:00
df1a06531f Merge pull request 'try new badge :-)' (#576) from add-badge into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 21m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 18m18s
Reviewed-on: #576
2024-06-06 10:48:51 +02:00
7b499fb457 try new badge :-)
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m55s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-06 10:38:18 +02:00
d00570ff2f Merge pull request 'nicer-cal' (#574) from nicer-cal into main
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) Successful in 6m0s
Reviewed-on: #574
2024-06-06 07:11:43 +02:00
bd63f2c386 use new additional information in test
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-06 06:59:41 +02:00
e1b78b2725 don't lose trip_type on event cancellation; don't add empty notes in cal
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-06-06 06:57:43 +02:00
fa14cfbf83 add cancellation, trip_type and notes to cal export
Some checks failed
CI/CD Pipeline / test (push) Failing after 9m56s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-06 06:47:41 +02:00
5f6cb9a12b Merge pull request 'update deps' (#572) from update-deps 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 17m43s
Reviewed-on: #572
2024-06-05 15:07:08 +02:00
1cac70cabb update deps
All checks were successful
CI/CD Pipeline / test (push) Successful in 19m14s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-05 14:57:28 +02:00
09cb8ebfa9 Merge pull request 'allow-event-triptype-update' (#570) from allow-event-triptype-update into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m20s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m12s
Reviewed-on: #570
2024-06-04 08:32:57 +02:00
ab88ce3230 fix tests
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m15s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-04 08:13:24 +02:00
30a6bc7109 allow event trip_type update
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-06-04 08:12:20 +02:00
99409f9407 Merge pull request 'remove space' (#568) from fix-spaces into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m42s
Reviewed-on: #568
2024-06-02 14:53:14 +02:00
7eff2a948a remove space
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-02 14:52:07 +02:00
243838fd44 Merge pull request 'remove debug println; better phrasing' (#566) from better-text 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: #566
2024-05-30 18:53:42 +02:00
2de4c86c26 remove debug println; better phrasing
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m26s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-30 11:57:03 +02:00
2889d40d55 Merge pull request 'better-text' (#564) from better-text 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 5m55s
Reviewed-on: #564
2024-05-30 11:52:17 +02:00
e8d4672176 try
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-05-30 11:34:01 +02:00
1bd643f6f4 try
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-05-30 11:11:08 +02:00
562c32939d better phrasing of text
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
2024-05-30 10:39:55 +02:00
3c0b8e5114 Merge pull request 'fix error' (#560) from fix-user-find-bug into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m28s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m56s
Reviewed-on: #560
2024-05-28 15:04:50 +02:00
6d5ff5404b fix error
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-05-28 15:04:06 +02:00
a8c0282918 Merge pull request 'migrated db' (#558) from migrated-db into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m28s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m56s
Reviewed-on: #558
2024-05-28 11:27:46 +02:00
9973913af6 migrated db
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-05-28 11:26:22 +02:00
7055c999e8 Merge pull request 'rename role to manage_events' (#556) from reanme-to-event 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: #556
2024-05-28 11:18:50 +02:00
c0d766832e rename role to manage_events
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) Has been skipped
2024-05-28 10:46:21 +02:00
f88c0be781 Merge pull request 'reanme-to-event' (#554) from reanme-to-event into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #554
2024-05-28 10:06:50 +02:00
91fa2a7762 add test for cancel-event-notification
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) Has been skipped
2024-05-28 09:59:16 +02:00
82aa94c024 rename planned_event to event
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) Has been skipped
2024-05-28 09:08:48 +02:00
aaf09208f3 Merge pull request 'don't care about cases for username' (#550) from case-insensitive-auth into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m33s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m15s
Reviewed-on: #550
2024-05-27 08:33:07 +02:00
86f7ca7065 don't care about cases for username
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
2024-05-27 08:32:00 +02:00
e325e0478a Merge pull request 'better spacing' (#548) from fix-spacing into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m34s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m57s
Reviewed-on: #548
2024-05-26 18:48:07 +02:00
0298617fc9 better spacing
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-05-26 18:47:09 +02:00
02ff89ba34 Merge pull request 'fix spacing' (#546) from fix-spacing 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 7m38s
Reviewed-on: #546
2024-05-26 14:18:13 +02:00
47a543fa64 fix spacing
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-05-26 14:17:26 +02:00
4e8fd84134 Merge pull request 'fix typo' (#544) from type into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m45s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m22s
Reviewed-on: #544
2024-05-25 18:53:00 +02:00
544267a037 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-05-25 18:52:00 +02:00
97b0ae83a9 Merge pull request 'extend log filter' (#540) from filter-logs into main
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) Successful in 8m20s
Reviewed-on: #540
2024-05-22 23:42:19 +02:00
f6d8c07c08 extend log filter
Some checks failed
CI/CD Pipeline / test (push) Has started running
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-05-22 23:41:24 +02:00
da56723909 Merge pull request 'fix footer' (#538) from fix-footer into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m27s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m0s
Reviewed-on: #538
2024-05-22 22:51:54 +02:00
603aed8394 fix footer
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-05-22 22:51:10 +02:00
f22d3b65be Merge pull request 'only allow realistic values for logbook entries' (#536) from sanity-check-timing 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: #536
2024-05-22 22:41:35 +02:00
446e48020e test fails due to new sanity check (25km in 1 min :-)); started trip 2 hours ago now in tests
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-05-22 22:28:33 +02:00
93e3e0ef5c only allow realistic values for logbook entries
Some checks failed
CI/CD Pipeline / test (push) Failing after 14m37s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-22 22:05:03 +02:00
8f5cc70981 Merge pull request 'automate schnupper mails' (#534) from automate-schnupper-mails into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m19s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m8s
Reviewed-on: #534
2024-05-22 08:51:41 +02:00
71c228f202 automate schnupper mails
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m15s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-22 08:30:38 +02:00
3ebde6afce Merge pull request 'allow to add reason for canceled event; fixes #530' (#532) from reason-for-canceled-event into main
Some checks failed
CI/CD Pipeline / test (push) Successful in 9m37s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #532
2024-05-22 08:13:58 +02:00
a797180b0d allow to add reason for canceled event; fixes #530
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m4s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-22 08:04:17 +02:00
9704893329 Merge pull request 'fix ci' (#529) from fix-ci 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 7m29s
Reviewed-on: #529
2024-05-22 00:24:46 +02:00
05c4c4f6a2 fix ci
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-05-22 00:23:51 +02:00
daf9460bf7 Merge pull request 'clean code with clippy' (#527) from clippy into main
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: #527
2024-05-22 00:18:47 +02:00
db5e0873a6 Merge branch 'staging' into clippy
Some checks failed
CI/CD Pipeline / test (push) Failing after 1m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-22 00:18:17 +02:00
40f97f18a9 clean code with clippy
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-05-22 00:13:23 +02:00
6b911f242a Merge pull request 'fix html duplicate tags' (#525) from fix-html into main
Some checks failed
CI/CD Pipeline / test (push) Successful in 10m7s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #525
2024-05-21 23:46:17 +02:00
f4ce748a74 Merge pull request 'fix html duplicate tags' (#524) from fix-html into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m47s
CI/CD Pipeline / deploy-staging (push) Successful in 7m45s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #524
2024-05-21 23:46:14 +02:00
d4ffd8850e fix html duplicate tags
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m56s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-21 23:35:36 +02:00
c93556a5ab Merge pull request 'allow cancel of events' (#522) from allow-cancel-of-events into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m8s
CI/CD Pipeline / deploy-staging (push) Successful in 7m40s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #522
2024-05-21 22:39:56 +02:00
96ce46d39c Merge branch 'main' into allow-cancel-of-events
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m47s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-21 22:19:33 +02:00
412ec27927 Merge pull request 'remove notificatoin about canceled trip if cancelation has been canceled' (#521) from remove-notification 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 7m36s
Reviewed-on: #521
2024-05-21 22:18:11 +02:00
a1c7e4c690 Merge pull request 'remove-notification' (#520) from remove-notification into staging
Some checks failed
CI/CD Pipeline / test (push) Successful in 9m25s
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #520
2024-05-21 22:17:56 +02:00
1f0de7abf4 allow cancel of 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-05-21 22:16:42 +02:00
64f3596132 remove notificatoin about canceled trip if cancelation has been canceled
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m35s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-21 20:43:05 +02:00
3b75f38dca Merge pull request 'remove users from trip after they have read the notificatoin' (#518) from remove-users-from-canceled-trips-after-reading-notification into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m49s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m30s
Reviewed-on: #518
2024-05-21 20:01:25 +02:00
10f2e3016a Merge pull request 'Merge pull request 'allow for sending notifications to single users' (#516) from allow-notifications-for-single-user into main' (#517) from remove-users-from-canceled-trips-after-reading-notification into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m44s
CI/CD Pipeline / deploy-staging (push) Successful in 9m24s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #517
2024-05-21 20:01:17 +02:00
1069e29cf0 fix ci
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m29s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-21 19:47:10 +02:00
b4967b54e9 remove users from trip after they have read the notificatoin
Some checks failed
CI/CD Pipeline / test (push) Failing after 3m50s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-21 19:41:00 +02:00
1285c3bc28 Merge pull request 'allow for sending notifications to single users' (#516) from allow-notifications-for-single-user into main
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #516
2024-05-21 19:27:41 +02:00
b6c9cb0b99 Merge pull request 'allow-notifications-for-single-user' (#515) from allow-notifications-for-single-user into staging
Some checks failed
CI/CD Pipeline / test (push) Successful in 9m13s
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
Reviewed-on: #515
2024-05-21 19:27:36 +02:00
29fabb04b0 allow for sending notifications to single users
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m28s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-21 18:57:24 +02:00
830aa58e7b Merge pull request 'don't allow to delete an event if someone is registered' (#514) from deletion-event-only-ok-noone-registered into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m7s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m37s
Reviewed-on: #514
2024-05-21 18:44:54 +02:00
1b6aec8d89 Merge pull request 'deletion-event-only-ok-noone-registered' (#513) from deletion-event-only-ok-noone-registered into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m14s
CI/CD Pipeline / deploy-staging (push) Successful in 8m2s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #513
2024-05-21 18:44:47 +02:00
1bf1cc9c68 inform user in case event can't be deleted
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m50s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-21 18:34:17 +02:00
02e1f77f65 don't allow to delete an event if someone is registered
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-05-21 18:28:56 +02:00
d819462b0d Merge pull request 'log if admin resets the pw for someone' (#512) from log-pw-reset 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: #512
2024-05-21 09:06:46 +02:00
387acdbd09 Merge pull request 'log if admin resets the pw for someone' (#511) from log-pw-reset into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m31s
CI/CD Pipeline / deploy-staging (push) Successful in 8m6s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #511
2024-05-21 09:06:37 +02:00
b36144832a log if admin resets the pw for someone
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-05-21 09:05:36 +02:00
fc49e6c977 Merge pull request 'inform coxes about status of boats' (#510) from notification-on-boatdamages into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m57s
Reviewed-on: #510
2024-05-20 21:21:56 +02:00
b8463122d6 Merge pull request 'notification-on-boatdamages' (#509) from notification-on-boatdamages into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m48s
CI/CD Pipeline / deploy-staging (push) Successful in 7m47s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #509
2024-05-20 21:21:42 +02:00
86db4cb2f4 inform coxes about status of boats
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) Has been skipped
2024-05-20 20:32:26 +02:00
82865799ce Merge pull request 'add impressum' (#505) from impressum into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m8s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m46s
Reviewed-on: #505
2024-05-17 15:35:33 +02:00
76f08905ab Merge pull request '[TASK] change layout imprint' (#508) from impressum into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m17s
CI/CD Pipeline / deploy-staging (push) Successful in 5m16s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #508
2024-05-17 13:46:17 +02:00
8dd878b492 [TASK] change layout imprint
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m31s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-17 13:44:09 +02:00
b405cf9936 Merge pull request '[TASK] change layout footer' (#507) from impressum 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: #507
2024-05-17 13:43:16 +02:00
01c2f0c4a3 [TASK] change layout footer
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-05-17 13:39:50 +02:00
0318d1dfb2 Merge pull request 'impressum' (#504) from impressum into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m58s
CI/CD Pipeline / deploy-staging (push) Successful in 6m4s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #504
2024-05-17 12:29:20 +02:00
261753c6b4 reformat
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-17 12:28:45 +02:00
d0038677ca add last data field
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-05-17 12:27:58 +02:00
4bd91b2a7e add impressum
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-05-17 12:26:10 +02:00
17f4291af0 Merge pull request 'marie-magic' (#501) from marie-magic into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m20s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m18s
Reviewed-on: #501
2024-05-16 22:46:42 +02:00
073f5aed0c Merge pull request 'marie-magic' (#500) from marie-magic into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m3s
CI/CD Pipeline / deploy-staging (push) Successful in 6m21s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #500
2024-05-16 22:46:39 +02:00
3097d99e00 remove unused imports
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m18s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-16 22:37:57 +02:00
0eac1a66f9 better layout
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-05-16 22:35:26 +02:00
f034f80794 Merge branch 'main' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt 2024-05-16 22:07:55 +02:00
57c9d532c8 push 2024-05-16 22:07:30 +02:00
b774acf9ae Merge pull request 'show info, if scheckbuch is not yet paid' (#499) from show-scheckbuch-info 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 9m8s
Reviewed-on: #499
2024-05-16 22:07:00 +02:00
20cc085562 Merge pull request 'show info, if scheckbuch is not yet paid' (#498) from show-scheckbuch-info into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m15s
CI/CD Pipeline / deploy-staging (push) Successful in 6m24s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #498
2024-05-16 21:40:30 +02:00
862ec5624a Merge pull request 'show waterlevel for the next days' (#466) from show-waterlevel into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Waiting to run
CI/CD Pipeline / deploy-main (push) Waiting to run
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #466
2024-05-16 21:32:23 +02:00
77a90a8086 Merge branch 'staging' into show-scheckbuch-info
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m18s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-16 21:31:12 +02:00
ca5a932ae5 show info, if scheckbuch is not yet paid
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-05-16 21:28:36 +02:00
626be1c9fb Merge pull request 'no rain level -> 0; round values' (#497) from show-waterlevel into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m0s
CI/CD Pipeline / deploy-staging (push) Successful in 5m58s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #497
2024-05-16 16:36:39 +02:00
7e2c185c03 no rain level -> 0; round values
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m17s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-16 16:35:49 +02:00
65068e44a5 Merge pull request 'show-waterlevel' (#496) from show-waterlevel into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m44s
CI/CD Pipeline / deploy-staging (push) Successful in 5m59s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #496
2024-05-16 14:59:05 +02:00
133a517a2e Merge branch 'staging' into show-waterlevel
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m33s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-16 14:47:50 +02:00
3d45310c73 Merge branch 'main' into show-waterlevel
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-05-16 14:45:31 +02:00
e4ef1f1584 add weather infos
Some checks failed
CI/CD Pipeline / test (push) Failing after 4m16s
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-05-16 14:41:15 +02:00
ebb4fe84bb Merge pull request 'allow removal of guests with special chars (e.g. questionamrk)' (#494) from fix-guest-encoding into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 22m6s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m41s
Reviewed-on: #494
2024-05-16 08:38:23 +02:00
2bf517ccd8 Merge pull request 'fix-guest-encoding' (#493) from fix-guest-encoding into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 23m39s
CI/CD Pipeline / deploy-staging (push) Successful in 7m37s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #493
2024-05-16 08:38:21 +02:00
1908f61268 allow removal of guests with special chars (e.g. questionamrk)
All checks were successful
CI/CD Pipeline / test (push) Successful in 18m24s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-16 08:17:53 +02:00
ac3301e97b Merge pull request 'add mail for scheckbuch people' (#492) from welcome-mail-scheckbuch into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m53s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m52s
Reviewed-on: #492
2024-05-15 16:20:19 +02:00
18faf4a72d Merge pull request 'welcome-mail-scheckbuch' (#491) from welcome-mail-scheckbuch into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m41s
CI/CD Pipeline / deploy-staging (push) Successful in 5m37s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #491
2024-05-15 16:20:12 +02:00
e3c30e010b add mail for scheckbuch people
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m36s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-15 16:01:01 +02:00
d9aa7cafe1 Merge pull request 'welcome-mail' (#490) from welcome-mail into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m23s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m11s
Reviewed-on: #490
2024-05-15 14:52:46 +02:00
97b0ce65f9 Merge pull request 'welcome-mail' (#489) from welcome-mail into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m13s
CI/CD Pipeline / deploy-staging (push) Successful in 6m3s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #489
2024-05-15 14:52:42 +02:00
a465dfcce5 reformat tera
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-05-15 14:42:58 +02:00
6371366a96 send welcome mail to new members
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-05-15 14:41:18 +02:00
0952bf7878 Merge pull request 'dont show guests on external boats' (#487) from dont-show-guests-on-external-boats into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m37s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m38s
Reviewed-on: #487
2024-05-12 23:08:48 +02:00
fa0dc5b544 Merge pull request 'dont-show-guests-on-external-boats' (#486) from dont-show-guests-on-external-boats into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m57s
CI/CD Pipeline / deploy-staging (push) Successful in 6m13s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #486
2024-05-12 23:08:41 +02:00
c3c7ecec98 fix ci
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-12 22:38:25 +02:00
a0d53366e0 dont show guests on external boats
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
CI/CD Pipeline / test (push) Failing after 5m15s
2024-05-12 22:26:02 +02:00
b69eded21d Merge pull request 'allow-membershippdf-upload' (#483) from allow-membershippdf-upload 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: #483
2024-05-06 13:46:40 +02:00
bd68bfc668 Merge pull request 'allow-membershippdf-upload' (#482) from allow-membershippdf-upload into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m21s
CI/CD Pipeline / deploy-staging (push) Successful in 4m47s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #482
2024-05-06 13:46:37 +02:00
7355d9d69b allow upload of membership pdf
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m6s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-06 13:35:42 +02:00
5602ad2681 Merge pull request 'only have a single user with details struct' (#481) from simplify-user-structs into main
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #481
2024-05-06 13:30:36 +02:00
6813d75db5 Merge pull request 'only have a single user with details struct' (#480) from simplify-user-structs into staging
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: #480
2024-05-06 13:30:33 +02:00
b4023c1ea8 Merge branch 'main' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt 2024-05-06 13:30:17 +02:00
45b51f4698 only have a single user with details struct
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m45s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-06 12:17:03 +02:00
31fda6bee9 Merge pull request 'trim name of new user name' (#479) from trim-new-user-names into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m49s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m23s
Reviewed-on: #479
2024-05-04 18:36:00 +02:00
e728c4dbea Merge pull request 'Merge pull request 'allow scheckbuch people to be entered in logbook' (#477) from allow-scheckbuch-to-be-entered into main' (#478) from trim-new-user-names into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m16s
CI/CD Pipeline / deploy-staging (push) Successful in 6m10s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #478
2024-05-04 18:35:48 +02:00
1d9adf071f trim name of new user name
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-04 18:19:07 +02:00
fcb4d65d32 Merge pull request 'allow scheckbuch people to be entered in logbook' (#477) from allow-scheckbuch-to-be-entered 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: #477
2024-05-01 19:37:14 +02:00
96036b180b Merge pull request 'allow-scheckbuch-to-be-entered' (#476) from allow-scheckbuch-to-be-entered into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m21s
CI/CD Pipeline / deploy-staging (push) Successful in 5m57s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #476
2024-05-01 19:37:09 +02:00
8c563a9c36 allow scheckbuch people to be entered in logbook
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m15s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-01 19:27:52 +02:00
b70929c5ce Merge pull request 'clippy :-)' (#475) from clippy into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m2s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m16s
Reviewed-on: #475
2024-04-30 22:26:09 +02:00
c98f33e138 Merge pull request 'clippy :-)' (#474) from clippy into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m20s
CI/CD Pipeline / deploy-staging (push) Successful in 6m16s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #474
2024-04-30 22:26:04 +02:00
17d1ee3566 clippy :-)
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m57s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-04-30 21:35:14 +02:00
a75ba765df Merge pull request 'don't use default distance of 11; don't overwrite distance if already entered' (#473) from logbook-entry-improvement into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m42s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m15s
Reviewed-on: #473
2024-04-30 21:31:33 +02:00
1503544a73 Merge pull request 'don't use default distance of 11; don't overwrite distance if already entered' (#472) from logbook-entry-improvement into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m12s
CI/CD Pipeline / deploy-staging (push) Successful in 6m17s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #472
2024-04-30 21:31:29 +02:00
0b350d344d don't use default distance of 11; don't overwrite distance if already entered
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m4s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-04-30 21:21:01 +02:00
61 changed files with 2714 additions and 907 deletions

View File

@ -25,7 +25,7 @@ jobs:
cargo build
cd frontend && npm install && npm run build
- name: Frontend tests
run: cd frontend && npx playwright test --workers 1 --reporter line
run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter line
- name: Backend tests
run: cargo test --verbose
#- uses: actions/upload-artifact@v3

520
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ rest = []
[dependencies]
rocket = { version = "0.5.0", features = ["secrets"]}
rocket_dyn_templates = {version = "0.1.0", features = [ "tera" ], optional = true }
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
log = "0.4"
env_logger = "0.11"
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
@ -24,9 +24,10 @@ ics = "0.5"
futures = "0.3"
lettre = "0.11"
csv = "1.3"
itertools = "0.12"
itertools = "0.13"
job_scheduler_ng = "2.0"
ureq = { version = "2.9", features = ["json"] }
regex = "1.10"
[target.'cfg(not(windows))'.dependencies]
openssl = { version = "0.10", features = [ "vendored" ] }

View File

@ -1,3 +1,5 @@
![latest CI run on main](https://git.hofer.link/Ruderverein-Donau-Linz/rowt/actions/workflows/action.yml/badge.svg?branch=main)
# Build
## Frontend
1. `cd frontend`

View File

@ -4,3 +4,4 @@ rss_key = "rss-key-for-ci"
limits = { file = "10 MiB", data-form = "10 MiB"}
smtp_pw = "8kIjlLH79Ky6D3jQ"
usage_log_path = "./usage.txt"
openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"

View File

@ -189,11 +189,6 @@ function selectBoatChange() {
inputElement.value = formattedDateTime;
const distinput = <HTMLInputElement>(
document.querySelector("#distance_in_km")
);
distinput.value = "";
const destinput = <HTMLInputElement>(
document.querySelector("#destination")
);
@ -759,9 +754,11 @@ function addRelationMagic(bodyElement: HTMLElement) {
},
);
if (option && option.value !== ""){
// Get distance
const distance = option.getAttribute("distance");
if (distance) relatedField.value = distance;
if (distance && relatedField.value === "") relatedField.value = distance;
}
}
});
}

View File

@ -67,6 +67,16 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
// Trip starts 2 hours ago
const datetimeSelector = '#departure';
const currentValue = await page.$eval(datetimeSelector, el => el.value);
const currentDate = new Date(currentValue);
currentDate.setMinutes(currentDate.getMinutes());
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
const newDatetime = currentDate.toISOString().slice(0, 16);
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
@ -80,15 +90,6 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
await page.goto("/log");
await page.locator("div:nth-child(2) > .border-0").click();
// Add a minute
const datetimeSelector = '#arrivaljs';
const currentValue = await page.$eval(datetimeSelector, el => el.value);
const currentDate = new Date(currentValue);
currentDate.setMinutes(currentDate.getMinutes() + 1);
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60);
const newDatetime = currentDate.toISOString().slice(0, 16);
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
await page.getByRole("combobox", { name: "Destination" }).click();
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
@ -153,6 +154,16 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
// Trip starts 2 hours ago
const datetimeSelector = '#departure';
const currentValue = await page.$eval(datetimeSelector, el => el.value);
const currentDate = new Date(currentValue);
currentDate.setMinutes(currentDate.getMinutes());
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
const newDatetime = currentDate.toISOString().slice(0, 16);
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
@ -166,15 +177,6 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
await page.goto("/log");
await page.locator('div:nth-child(2) > .pt-2 > div > div > div:nth-child(2) > .border-0').click(); // 2 trips currently running, try to close second one
// Add a minute
const datetimeSelector = '#arrivaljs';
const currentValue = await page.$eval(datetimeSelector, el => el.value);
const currentDate = new Date(currentValue);
currentDate.setMinutes(currentDate.getMinutes() + 1);
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60);
const newDatetime = currentDate.toISOString().slice(0, 16);
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
await page.getByRole("combobox", { name: "Destination" }).click();
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();

View File

@ -160,6 +160,7 @@ CREATE TABLE IF NOT EXISTS "notification" (
"read_at" DATETIME,
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
"category" TEXT NOT NULL,
"action_after_reading" TEXT,
"link" TEXT
);
@ -186,3 +187,29 @@ CREATE TABLE IF NOT EXISTS "waterlevel" (
"tumin" INTEGER NOT NULL,
"tumittel" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS "weather" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"day" DATE NOT NULL,
"max_temp" FLOAT NOT NULL,
"wind_gust" FLOAT NOT NULL,
"rain_mm" FLOAT NOT NULL
);
CREATE TABLE IF NOT EXISTS "trailer" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS "trailer_reservation" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"trailer_id" INTEGER NOT NULL REFERENCES trailer(id),
"start_date" DATE NOT NULL,
"end_date" DATE NOT NULL,
"time_desc" TEXT NOT NULL,
"usage" TEXT NOT NULL,
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
"user_id_confirmation" INTEGER REFERENCES user(id),
"created_at" datetime not null default CURRENT_TIMESTAMP
);

View File

@ -3,11 +3,12 @@ INSERT INTO "role" (name) VALUES ('cox');
INSERT INTO "role" (name) VALUES ('scheckbuch');
INSERT INTO "role" (name) VALUES ('tech');
INSERT INTO "role" (name) VALUES ('Donau Linz');
INSERT INTO "role" (name) VALUES ('planned_event');
INSERT INTO "role" (name) VALUES ('manage_events');
INSERT INTO "role" (name) VALUES ('Rennrudern');
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 "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);
@ -63,3 +64,5 @@ INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3);
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02');
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1);
INSERT INTO "notification" (user_id, message, category) VALUES (1, 'This is a test notification', 'test-cat');
INSERT INTO "trailer" (name) VALUES('Großer Hänger');
INSERT INTO "trailer" (name) VALUES('Kleiner Hänger');

View File

@ -4,9 +4,9 @@ use std::str::FromStr;
#[cfg(feature = "rest")]
use rot::rest;
use rot::scheduled;
#[cfg(feature = "rowing-tera")]
use rot::tera;
use rot::{scheduled, tera::Config};
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
@ -27,9 +27,7 @@ async fn rocket() -> _ {
.await
.unwrap();
scheduled::schedule(&db);
let rocket = rocket::build().manage(db);
let rocket = rocket::build().manage(db.clone());
#[cfg(feature = "rowing-tera")]
let rocket = tera::config(rocket);
@ -37,5 +35,11 @@ async fn rocket() -> _ {
#[cfg(feature = "rest")]
let rocket = rest::config(rocket);
let config: Config = rocket
.figment()
.extract()
.expect("Config extraction failed");
scheduled::schedule(&db, &config);
rocket
}

View File

@ -81,20 +81,20 @@ pub struct BoatToUpdate<'r> {
impl Boat {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM boat WHERE name like ?", name)
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE name like ?", name)
.fetch_one(db)
.await
.ok()
@ -181,13 +181,11 @@ AND date('now') BETWEEN start_date AND end_date;",
damage = BoatDamage::Locked;
}
let cat = if boat.external {
format!("Vereinsfremde Boote")
"Vereinsfremde Boote".to_string()
} else if boat.default_shipmaster_only_steering {
format!("{}+", boat.amount_seats - 1)
} else {
if boat.default_shipmaster_only_steering {
format!("{}+", boat.amount_seats - 1)
} else {
format!("{}x", boat.amount_seats)
}
format!("{}x", boat.amount_seats)
};
res.push(BoatWithDetails {

View File

@ -119,6 +119,10 @@ ORDER BY created_at DESC
pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> {
Log::create(db, format!("New boat damage: {boatdamage:?}")).await;
let Some(boat) = Boat::find_by_id(db, boatdamage.boat_id as i32).await else {
return Err("Boot gibt's ned".into());
};
let was_unusable_before = boat.is_locked(db).await;
sqlx::query!(
"INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)",
@ -131,6 +135,11 @@ ORDER BY created_at DESC
.await
.map_err(|e| e.to_string())?;
if !was_unusable_before && boat.is_locked(db).await {
let cox = Role::find_by_name(db, "cox").await.unwrap();
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).await;
}
let technicals =
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
for technical in technicals {
@ -144,14 +153,12 @@ ORDER BY created_at DESC
.await
.unwrap()
.name,
Boat::find_by_id(db, boatdamage.boat_id as i32)
.await
.unwrap()
.name,
boat.name,
boatdamage.desc
),
"Neuer Bootsschaden angelegt",
None,
None,
)
.await;
}
@ -172,6 +179,7 @@ ORDER BY created_at DESC
),
"Neuer Bootsschaden angelegt",
None,
None,
)
.await;
@ -229,7 +237,7 @@ ORDER BY created_at DESC
boat.name,
),
"Bootsschaden repariert",
None,
None,None
)
.await;
}
@ -255,7 +263,7 @@ ORDER BY created_at DESC
boat_damage.desc, boat.name,
),
"Bootsschaden repariert",
None,
None,None
)
.await;
}
@ -267,35 +275,41 @@ ORDER BY created_at DESC
pub async fn verified(
&self,
db: &SqlitePool,
boat: BoatDamageVerified<'_>,
boat_form: BoatDamageVerified<'_>,
) -> Result<(), String> {
if let Some(verifier) = User::find_by_id(db, boat.user_id_verified).await {
if let Some(verifier) = User::find_by_id(db, boat_form.user_id_verified).await {
if !verifier.has_role(db, "tech").await {
Log::create(db, format!("User {verifier:?} tried to verify boat {boat:?}. The user is no tech. Manually craftted request?")).await;
Log::create(db, format!("User {verifier:?} tried to verify boat {boat_form:?}. The user is no tech. Manually craftted request?")).await;
return Err("You are not allowed to verify the boat!".into());
}
} else {
Log::create(db, format!("Someone tried to verify the boat {boat:?} with user_id={} which does not exist. Manually craftted request?", boat.user_id_verified)).await;
Log::create(db, format!("Someone tried to verify the boat {boat_form:?} with user_id={} which does not exist. Manually craftted request?", boat_form.user_id_verified)).await;
return Err("Could not find user".into());
}
Log::create(db, format!("Verified boat damage: {boat:?}")).await;
let Some(boat) = Boat::find_by_id(db, self.boat_id as i32).await else {
return Err("Boot gibt's ned".into());
};
let was_unusable_before = boat.is_locked(db).await;
Log::create(db, format!("Verified boat damage: {boat_form:?}")).await;
sqlx::query!(
"UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?",
boat.desc,
boat.user_id_verified,
boat_form.desc,
boat_form.user_id_verified,
self.id
)
.execute(db)
.await.map_err(|e| e.to_string())?;
if boat.user_id_verified != self.user_id_created as i32 {
let user_verified = User::find_by_id(db, boat.user_id_verified).await.unwrap();
if boat_form.user_id_verified != self.user_id_created as i32 {
let user_verified = User::find_by_id(db, boat_form.user_id_verified)
.await
.unwrap();
let user_created = User::find_by_id(db, self.user_id_created as i32)
.await
.unwrap();
let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap();
if user_verified.id == self.user_id_fixed.unwrap() {
Notification::create(
@ -308,6 +322,7 @@ ORDER BY created_at DESC
),
"Bootsschaden repariert & verifiziert",
None,
None
)
.await;
} else {
@ -321,10 +336,16 @@ ORDER BY created_at DESC
),
"Bootsschaden verifiziert",
None,
None
).await;
}
}
if was_unusable_before && !boat.is_locked(db).await {
let cox = Role::find_by_name(db, "cox").await.unwrap();
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, {} wurde repariert und freut sich ab sofort wieder gerudert zu werden :-)", boat.name), "Boot repariert", None, None).await;
}
Ok(())
}
}

View File

@ -114,7 +114,7 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
grouped_reservations
.entry(key)
.or_insert_with(Vec::new)
.or_default()
.push(reservation);
}
@ -175,7 +175,7 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
boatreservation.usage
),
"Neue Bootsreservierung",
None,
None,None
)
.await;
}

View File

@ -3,33 +3,33 @@ use std::io::Write;
use chrono::NaiveDate;
use ics::{
properties::{DtStart, Summary},
Event, ICalendar,
ICalendar,
};
use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool};
use super::{tripdetails::TripDetails, triptype::TripType, user::User};
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
pub struct PlannedEvent {
pub struct Event {
pub id: i64,
pub name: String,
planned_amount_cox: i64,
pub(crate) planned_amount_cox: i64,
trip_details_id: i64,
pub planned_starting_time: String,
max_people: i64,
pub(crate) max_people: i64,
pub day: String,
pub notes: Option<String>,
pub allow_guests: bool,
trip_type_id: Option<i64>,
always_show: bool,
is_locked: bool,
pub(crate) always_show: bool,
pub(crate) is_locked: bool,
}
#[derive(Serialize, Debug)]
pub struct PlannedEventWithUserAndTriptype {
pub struct EventWithUserAndTriptype {
#[serde(flatten)]
pub planned_event: PlannedEvent,
pub event: Event,
trip_type: Option<TripType>,
cox_needed: bool,
cox: Vec<Registration>,
@ -73,7 +73,7 @@ FROM user_trip WHERE trip_details_id = {}
.collect()
}
pub async fn all_cox(db: &SqlitePool, trip_details_id: i64) -> Vec<Registration> {
pub async fn all_cox(db: &SqlitePool, event_id: i64) -> Vec<Registration> {
//TODO: switch to join
sqlx::query!(
"
@ -82,7 +82,7 @@ SELECT
(SELECT created_at FROM user WHERE cox_id = id) as registered_at
FROM trip WHERE planned_event_id = ?
",
trip_details_id
event_id
)
.fetch_all(db)
.await
@ -94,11 +94,22 @@ FROM trip WHERE planned_event_id = ?
is_guest: false,
is_real_guest: false,
})
.collect() //Okay, as PlannedEvent can only be created with proper DB backing
.collect() //Okay, as Event can only be created with proper DB backing
}
}
impl PlannedEvent {
#[derive(Debug)]
pub struct EventUpdate<'a> {
pub name: &'a str,
pub planned_amount_cox: i32,
pub max_people: i32,
pub notes: Option<&'a str>,
pub always_show: bool,
pub is_locked: bool,
pub trip_type_id: Option<i64>,
}
impl Event {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
@ -119,19 +130,16 @@ WHERE planned_event.id like ?
pub async fn get_pinned_for_day(
db: &SqlitePool,
day: NaiveDate,
) -> Vec<PlannedEventWithUserAndTriptype> {
) -> Vec<EventWithUserAndTriptype> {
let mut events = Self::get_for_day(db, day).await;
events.retain(|e| e.planned_event.always_show);
events.retain(|e| e.event.always_show);
events
}
pub async fn get_for_day(
db: &SqlitePool,
day: NaiveDate,
) -> Vec<PlannedEventWithUserAndTriptype> {
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithUserAndTriptype> {
let day = format!("{day}");
let events = sqlx::query_as!(
PlannedEvent,
Event,
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
@ -149,20 +157,20 @@ WHERE day=?",
if let Some(trip_type_id) = event.trip_type_id {
trip_type = TripType::find_by_id(db, trip_type_id).await;
}
ret.push(PlannedEventWithUserAndTriptype {
ret.push(EventWithUserAndTriptype {
cox_needed: event.planned_amount_cox > cox.len() as i64,
cox,
rower: Registration::all_rower(db, event.trip_details_id).await,
planned_event: event,
event,
trip_type,
});
}
ret
}
pub async fn all(db: &SqlitePool) -> Vec<PlannedEvent> {
pub async fn all(db: &SqlitePool) -> Vec<Event> {
sqlx::query_as!(
PlannedEvent,
Event,
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
@ -189,11 +197,27 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
is_rower.amount > 0
}
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
WHERE trip_details.id=?
",
tripdetails_id
)
.fetch_one(db)
.await
.ok()
}
pub async fn create(
db: &SqlitePool,
name: &str,
planned_amount_cox: i32,
trip_details: TripDetails,
trip_details: &TripDetails,
) {
sqlx::query!(
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
@ -207,58 +231,153 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
}
//TODO: create unit test
pub async fn update(
&self,
db: &SqlitePool,
name: &str,
planned_amount_cox: i32,
max_people: i32,
notes: Option<&str>,
always_show: bool,
is_locked: bool,
) {
pub async fn update(&self, db: &SqlitePool, update: &EventUpdate<'_>) {
sqlx::query!(
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
name,
planned_amount_cox,
update.name,
update.planned_amount_cox,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
let tripdetails = self.trip_details(db).await;
let was_already_cancelled = tripdetails.max_people == 0;
sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ? WHERE id = ?",
max_people,
notes,
always_show,
is_locked,
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
update.max_people,
update.notes,
update.always_show,
update.is_locked,
update.trip_type_id,
self.trip_details_id
)
.execute(db)
.await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
if update.max_people == 0 && !was_already_cancelled {
let coxes = Registration::all_cox(db, self.id).await;
for user in coxes {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match update.notes {
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
_ => String::from(""),
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
self.name, self.day, self.planned_starting_time, notes
),
"Absage Ausfahrt",
None,
Some(&format!("remove_trip_by_event:{}", self.id)),
)
.await;
}
}
let rower = Registration::all_rower(db, self.trip_details_id).await;
for user in rower {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match update.notes {
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
_ => String::from(""),
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
self.name, self.day, self.planned_starting_time, notes
),
"Absage Ausfahrt",
None,
Some(&format!(
"remove_user_trip_with_trip_details_id:{}",
tripdetails.id
)),
)
.await;
}
}
}
if update.max_people > 0 && was_already_cancelled {
Notification::delete_by_action(
db,
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
)
.await;
Notification::delete_by_action(db, &format!("remove_trip_by_event:{}", self.id)).await;
}
}
pub async fn delete(&self, db: &SqlitePool) {
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
if !Registration::all_rower(db, self.trip_details_id)
.await
.is_empty()
{
return Err(
"Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(),
);
}
if !Registration::all_cox(db, self.trip_details_id)
.await
.is_empty()
{
return Err(
"Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist."
.into(),
);
}
sqlx::query!("DELETE FROM planned_event WHERE id = ?", self.id)
.execute(db)
.await
.unwrap(); //Okay, as PlannedEvent can only be created with proper DB backing
.unwrap(); //Okay, as Event can only be created with proper DB backing
Ok(())
}
pub fn is_cancelled(&self) -> bool {
self.max_people == 0
}
pub async fn get_ics_feed(db: &SqlitePool) -> String {
let mut calendar = ICalendar::new("2.0", "ics-rs");
let events = PlannedEvent::all(db).await;
let events = Event::all(db).await;
for event in events {
let mut vevent = Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
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(':', "")
)));
vevent.push(Summary::new(event.name));
let tripdetails = event.trip_details(db).await;
let mut name = String::new();
if event.is_cancelled() {
name.push_str("ABGESAGT");
if let Some(notes) = &tripdetails.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
name.push_str(&format!("{} ", event.name));
if let Some(triptype) = tripdetails.triptype(db).await {
name.push_str(&format!("{} ", triptype.name))
}
vevent.push(Summary::new(name));
calendar.add_event(vevent);
}
let mut buf = Vec::new();
@ -277,7 +396,7 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
mod test {
use crate::{model::tripdetails::TripDetails, testdb};
use super::PlannedEvent;
use super::Event;
use chrono::NaiveDate;
use sqlx::SqlitePool;
@ -285,8 +404,7 @@ mod test {
fn test_get_day() {
let pool = testdb!();
let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 1);
}
@ -296,22 +414,20 @@ mod test {
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
PlannedEvent::create(&pool, "new-event".into(), 2, trip_details).await;
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 2);
}
#[sqlx::test]
fn test_delete() {
let pool = testdb!();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
planned_event.delete(&pool).await;
planned_event.delete(&pool).await.unwrap();
let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 0);
}
@ -319,7 +435,7 @@ mod test {
fn test_ics() {
let pool = testdb!();
let actual = PlannedEvent::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);
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);
}
}

View File

@ -104,6 +104,7 @@ pub enum LogbookUpdateError {
SteeringPersonNotInRowers,
UserNotAllowedToUseBoat,
OnlyAllowedToEndTripsEndingToday,
TooFast(i64, i64),
}
#[derive(Debug, PartialEq)]
@ -118,7 +119,7 @@ pub enum LogbookCreateError {
BoatLocked,
BoatNotFound,
TooManyRowers(usize, usize),
RowerAlreadyOnWater(User),
RowerAlreadyOnWater(Box<User>),
RowerCreateError(i64, String),
ArrivalNotAfterDeparture,
SteeringPersonNotInRowers,
@ -127,6 +128,7 @@ pub enum LogbookCreateError {
ArrivalSetButNotRemainingTwo,
OnlyAllowedToEndTripsEndingToday,
CantChangeHandoperatableStatusForThisBoat,
TooFast(i64, i64),
}
impl From<LogbookUpdateError> for LogbookCreateError {
@ -150,6 +152,7 @@ impl From<LogbookUpdateError> for LogbookCreateError {
LogbookUpdateError::OnlyAllowedToEndTripsEndingToday => {
LogbookCreateError::OnlyAllowedToEndTripsEndingToday
}
LogbookUpdateError::TooFast(km, min) => LogbookCreateError::TooFast(km, min),
}
}
}
@ -298,15 +301,15 @@ ORDER BY departure DESC
db: &SqlitePool,
mut log: LogToAdd,
created_by_user: &User,
) -> Result<(), LogbookCreateError> {
) -> Result<String, LogbookCreateError> {
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
return Err(LogbookCreateError::BoatNotFound);
};
if log.shipmaster_only_steering != boat.default_shipmaster_only_steering {
if !boat.convert_handoperated_possible {
return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat);
}
if log.shipmaster_only_steering != boat.default_shipmaster_only_steering
&& !boat.convert_handoperated_possible
{
return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat);
}
if boat.amount_seats == 1 && log.rowers.is_empty() {
@ -351,7 +354,7 @@ ORDER BY departure DESC
{
Ok(_) => {
tx.commit().await.unwrap();
Ok(())
Ok(String::new())
}
Err(a) => Err(a.into()),
};
@ -386,7 +389,7 @@ ORDER BY departure DESC
let user = User::find_by_id(db, *rower as i32).await.unwrap();
if user.on_water(db).await {
return Err(LogbookCreateError::RowerAlreadyOnWater(user));
return Err(LogbookCreateError::RowerAlreadyOnWater(Box::new(user)));
}
}
@ -423,7 +426,15 @@ ORDER BY departure DESC
tx.commit().await.unwrap();
Ok(())
let mut ret = String::new();
for rower in &log.rowers {
let user = User::find_by_id(db, *rower as i32).await.unwrap();
if let Some(msg) = user.close_thousands_trip(db).await {
ret.push_str(&format!("{msg}"));
}
}
Ok(ret)
}
pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> {
@ -517,6 +528,17 @@ ORDER BY departure DESC
if arr.and_utc().timestamp() < dep.and_utc().timestamp() {
return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
}
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;
if log.distance_in_km > possible_distance_km {
return Err(LogbookUpdateError::TooFast(
log.distance_in_km,
duration_in_mins,
));
}
let today = Local::now().date_naive();
let day_diff = today - arr.date();
let day_diff = day_diff.num_days();
@ -549,6 +571,7 @@ ORDER BY departure DESC
),
"Neuer Logbucheintrag",
None,
None,
)
.await;
}
@ -578,7 +601,7 @@ ORDER BY departure DESC
&vorstand,
&format!("'{}' hat eine mehrtägige Ausfahrt vom {} bis {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,log.departure, log.arrival, log.distance_in_km, log.destination, log.comments.clone().unwrap_or("".into())),
"Mehrtägige Ausfahrt eingetragen",
None,
None,None
).await;
}
@ -590,7 +613,7 @@ ORDER BY departure DESC
&vorstand,
&format!("'{}' hat eine Ausfahrt mit externem Boot '{}' am {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,boat.name,log.departure,log.distance_in_km, log.destination, log.comments.unwrap_or("".into())),
"Ausfahrt mit externem Boot eingetragen",
None,
None,None,
).await;
}
@ -620,6 +643,7 @@ mod test {
use crate::model::user::User;
use crate::testdb;
use chrono::Duration;
use sqlx::SqlitePool;
#[sqlx::test]
@ -671,7 +695,7 @@ mod test {
fn test_succ_create() {
let pool = testdb!();
Logbook::create(
let msg = Logbook::create(
&pool,
LogToAdd {
boat_id: 3,
@ -689,7 +713,62 @@ mod test {
&User::find_by_id(&pool, 4).await.unwrap(),
)
.await
.unwrap()
.unwrap();
assert_eq!(msg, String::from(""));
}
#[sqlx::test]
fn test_succ_create_with_thousands_msg() {
let pool = testdb!();
let logbook = Logbook::find_by_id(&pool, 1).await.unwrap();
let user = User::find_by_id(&pool, 2).await.unwrap();
let current_date = chrono::Local::now().format("%Y-%m-%d").to_string();
let start_date = chrono::Local::now() - Duration::days(3);
let start_date = start_date.format("%Y-%m-%d").to_string();
logbook
.home(
&pool,
&user,
super::LogToFinalize {
destination: "new-destination".into(),
distance_in_km: 995,
comments: Some("Perfect water".into()),
logtype: None,
rowers: vec![2],
shipmaster: Some(2),
steering_person: Some(2),
shipmaster_only_steering: false,
departure: format!("{}T10:00", start_date),
arrival: format!("{}T12:00", current_date),
},
)
.await
.unwrap();
let msg = Logbook::create(
&pool,
LogToAdd {
boat_id: 3,
shipmaster: Some(2),
steering_person: Some(2),
shipmaster_only_steering: false,
departure: "2128-05-20T12:00".into(),
arrival: None,
destination: None,
distance_in_km: None,
comments: None,
logtype: None,
rowers: vec![2],
},
&User::find_by_id(&pool, 1).await.unwrap(),
)
.await
.unwrap();
assert_eq!(
msg,
String::from(" • rower braucht nur mehr 5 km bis die 1000 km voll sind 🤑")
);
}
#[sqlx::test]

View File

@ -14,6 +14,63 @@ use super::{family::Family, log::Log, role::Role, user::User};
pub struct Mail {}
impl Mail {
pub async fn send_single(
db: &SqlitePool,
to: &str,
subject: &str,
body: String,
smtp_pw: &str,
) -> Result<(), String> {
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = to.split(',');
for single_rec in splitted {
match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => {
Log::create(
db,
format!("Mail not sent to {single_rec}, because it could not be parsed"),
)
.await;
return Err(format!(
"Mail nicht versandt, da '{single_rec}' keine gültige Mailadresse ist."
));
}
}
}
let email = email
.subject(subject)
.header(ContentType::TEXT_PLAIN)
.body(body)
.unwrap();
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into());
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
mailer.send(&email).unwrap();
Ok(())
}
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
let mut email = Message::builder()
.from(

View File

@ -1,17 +1,20 @@
use chrono::NaiveDate;
use serde::Serialize;
use sqlx::SqlitePool;
use waterlevel::WaterlevelDay;
use self::{
planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype},
event::{Event, EventWithUserAndTriptype},
trip::{Trip, TripWithUserAndType},
waterlevel::Waterlevel,
weather::Weather,
};
pub mod boat;
pub mod boatdamage;
pub mod boathouse;
pub mod boatreservation;
pub mod event;
pub mod family;
pub mod location;
pub mod log;
@ -19,24 +22,27 @@ pub mod logbook;
pub mod logtype;
pub mod mail;
pub mod notification;
pub mod planned_event;
pub mod role;
pub mod rower;
pub mod stat;
pub mod trailer;
pub mod trailerreservation;
pub mod trip;
pub mod tripdetails;
pub mod triptype;
pub mod user;
pub mod usertrip;
pub mod waterlevel;
pub mod weather;
#[derive(Serialize, Debug)]
pub struct Day {
day: NaiveDate,
planned_events: Vec<PlannedEventWithUserAndTriptype>,
events: Vec<EventWithUserAndTriptype>,
trips: Vec<TripWithUserAndType>,
is_pinned: bool,
max_waterlevel: Option<i64>,
max_waterlevel: Option<WaterlevelDay>,
weather: Option<Weather>,
}
impl Day {
@ -44,25 +50,27 @@ impl Day {
if is_pinned {
Self {
day,
planned_events: PlannedEvent::get_pinned_for_day(db, day).await,
events: Event::get_pinned_for_day(db, day).await,
trips: Trip::get_pinned_for_day(db, day).await,
is_pinned,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await,
}
} else {
Self {
day,
planned_events: PlannedEvent::get_for_day(db, day).await,
events: Event::get_for_day(db, day).await,
trips: Trip::get_for_day(db, day).await,
is_pinned,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await,
}
}
}
pub async fn new_guest(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
let mut day = Self::new(db, day, is_pinned).await;
day.planned_events.retain(|e| e.planned_event.allow_guests);
day.events.retain(|e| e.event.allow_guests);
day.trips.retain(|t| t.trip.allow_guests);
day

View File

@ -1,12 +1,13 @@
use std::ops::DerefMut;
use chrono::NaiveDateTime;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{role::Role, user::User};
#[derive(FromRow, Debug, Serialize, Deserialize)]
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct Notification {
pub id: i64,
pub user_id: i64,
@ -15,11 +16,12 @@ pub struct Notification {
pub created_at: NaiveDateTime,
pub category: String,
pub link: Option<String>,
pub action_after_reading: Option<String>,
}
impl Notification {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM notification WHERE id like ?", id)
sqlx::query_as!(Self, "SELECT id, user_id, message, read_at, created_at, category, link, action_after_reading FROM notification WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
@ -30,13 +32,15 @@ impl Notification {
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
sqlx::query!(
"INSERT INTO notification(user_id, message, category, link) VALUES (?, ?, ?, ?)",
"INSERT INTO notification(user_id, message, category, link, action_after_reading) VALUES (?, ?, ?, ?, ?)",
user.id,
message,
category,
link
link,
action_after_reading
)
.execute(db.deref_mut())
.await
@ -49,9 +53,10 @@ impl Notification {
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let mut tx = db.begin().await.unwrap();
Self::create_with_tx(&mut tx, user, message, category, link).await;
Self::create_with_tx(&mut tx, user, message, category, link, action_after_reading).await;
tx.commit().await.unwrap();
}
@ -61,18 +66,33 @@ impl Notification {
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let users = User::all_with_role_tx(db, role).await;
for user in users {
Self::create_with_tx(db, &user, message, category, link).await;
Self::create_with_tx(db, &user, message, category, link, action_after_reading).await;
}
}
pub async fn create_for_role(
db: &SqlitePool,
role: &Role,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let mut tx = db.begin().await.unwrap();
Self::create_for_role_tx(&mut tx, role, message, category, link, action_after_reading)
.await;
tx.commit().await.unwrap();
}
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
let rows = sqlx::query!(
"
SELECT id, user_id, message, read_at, datetime(created_at, 'localtime') as created_at, category, link FROM notification
SELECT id, user_id, message, read_at, datetime(created_at, 'localtime') as created_at, category, link, action_after_reading FROM notification
WHERE
user_id = ?
AND (
@ -101,6 +121,7 @@ ORDER BY read_at DESC, created_at DESC;
.unwrap(),
category: rec.category,
link: rec.link,
action_after_reading: rec.action_after_reading,
})
.collect()
}
@ -113,5 +134,162 @@ ORDER BY read_at DESC, created_at DESC;
.execute(db)
.await
.unwrap();
if let Some(action) = self.action_after_reading.as_ref() {
// User read notification about cancelled trip/event
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();
}
}
}
// Cox read notification about cancelled event
let re = Regex::new(r"^remove_trip_by_event:(\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 trip WHERE cox_id = ? AND planned_event_id = ?",
self.user_id,
number
)
.execute(db)
.await
.unwrap();
}
}
}
}
}
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
sqlx::query!(
"DELETE FROM notification WHERE action_after_reading=? and read_at is null",
action
)
.execute(db)
.await
.unwrap();
}
}
#[cfg(test)]
mod test {
use crate::{
model::{
event::{Event, EventUpdate, Registration},
notification::Notification,
trip::Trip,
tripdetails::{TripDetails, TripDetailsToAdd},
user::{CoxUser, User},
usertrip::UserTrip,
},
testdb,
};
use sqlx::SqlitePool;
#[sqlx::test]
fn event_canceled() {
let pool = testdb!();
// Create event
let add_tripdetails = TripDetailsToAdd {
planned_starting_time: "10:00",
max_people: 4,
day: "1970-02-01".into(),
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;
let event = Event::find_by_trip_details(&pool, trip_details.id)
.await
.unwrap();
// Rower + Cox joins
let rower = User::find_by_name(&pool, "rower").await.unwrap();
UserTrip::create(&pool, &rower, &trip_details, None)
.await
.unwrap();
let cox = CoxUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap())
.await
.unwrap();
Trip::new_join(&pool, &cox, &event).await.unwrap();
// Cancel Event
let cancel_update = EventUpdate {
name: &event.name,
planned_amount_cox: event.planned_amount_cox as i32,
max_people: 0,
notes: event.notes.as_deref(),
always_show: event.always_show,
is_locked: event.is_locked,
trip_type_id: None,
};
event.update(&pool, &cancel_update).await;
// Rower received notification
let notifications = Notification::for_user(&pool, &rower).await;
let rower_notification = notifications[0].clone();
assert_eq!(rower_notification.category, "Absage Ausfahrt");
assert_eq!(
rower_notification.action_after_reading.as_deref(),
Some("remove_user_trip_with_trip_details_id:3")
);
// Cox received notification
let notifications = Notification::for_user(&pool, &cox.user).await;
let cox_notification = notifications[0].clone();
assert_eq!(cox_notification.category, "Absage Ausfahrt");
assert_eq!(
cox_notification.action_after_reading.as_deref(),
Some("remove_trip_by_event:2")
);
// Notification removed if cancellation is cancelled
let update = EventUpdate {
name: &event.name,
planned_amount_cox: event.planned_amount_cox as i32,
max_people: 3,
notes: event.notes.as_deref(),
always_show: event.always_show,
is_locked: event.is_locked,
trip_type_id: None,
};
event.update(&pool, &update).await;
assert!(Notification::for_user(&pool, &rower).await.is_empty());
assert!(Notification::for_user(&pool, &cox.user).await.is_empty());
// Cancel event again
event.update(&pool, &cancel_update).await;
// Rower is removed if notification is accepted
assert!(event.is_rower_registered(&pool, &rower).await);
rower_notification.mark_read(&pool).await;
assert!(!event.is_rower_registered(&pool, &rower).await);
// Cox is removed if notification is accepted
let registration = Registration::all_cox(&pool, event.id).await;
assert_eq!(registration.len(), 1);
assert_eq!(registration[0].name, "cox");
cox_notification.mark_read(&pool).await;
let registration = Registration::all_cox(&pool, event.id).await;
assert!(registration.is_empty());
}
}

View File

@ -95,7 +95,7 @@ ORDER BY
#[derive(FromRow, Serialize, Clone)]
pub struct Stat {
name: String,
rowed_km: i32,
pub(crate) rowed_km: i32,
}
impl Stat {
@ -195,6 +195,34 @@ ORDER BY rowed_km DESC, u.name;
})
.collect()
}
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
let year = match year {
Some(year) => year,
None => chrono::Local::now().year(),
};
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let row = sqlx::query(&format!(
"
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM (
SELECT * FROM user
WHERE id={}
) u
INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
",
user.id
))
.fetch_one(db)
.await
.unwrap();
Stat {
name: row.get("name"),
rowed_km: row.get("rowed_km"),
}
}
}
#[derive(Debug, Serialize)]
@ -218,7 +246,7 @@ FROM (
LEFT JOIN
rower r ON l.id = r.logbook_id
WHERE
l.shipmaster = {0} OR r.rower_id = {0}
r.rower_id = {}
GROUP BY
departure_date
) as subquery

31
src/model/trailer.rs Normal file
View File

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

View File

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

View File

@ -3,8 +3,8 @@ use serde::Serialize;
use sqlx::SqlitePool;
use super::{
event::{Event, Registration},
notification::Notification,
planned_event::{PlannedEvent, Registration},
tripdetails::TripDetails,
triptype::TripType,
user::{CoxUser, User},
@ -34,6 +34,16 @@ pub struct TripWithUserAndType {
trip_type: Option<TripType>,
}
pub struct TripUpdate<'a> {
pub cox: &'a CoxUser,
pub trip: &'a Trip,
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,
}
impl TripWithUserAndType {
pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
let mut trip_type = None;
@ -80,6 +90,7 @@ impl Trip {
),
"Neue Ausfahrt zur selben Zeit",
None,
None,
)
.await;
}
@ -122,24 +133,28 @@ WHERE trip.id=?
.ok()
}
/// Cox decides to help in a planned event.
/// Cox decides to help in a event.
pub async fn new_join(
db: &SqlitePool,
cox: &CoxUser,
planned_event: &PlannedEvent,
event: &Event,
) -> Result<(), CoxHelpError> {
if planned_event.is_rower_registered(db, cox).await {
if event.is_rower_registered(db, cox).await {
return Err(CoxHelpError::AlreadyRegisteredAsRower);
}
if planned_event.trip_details(db).await.is_locked {
if event.trip_details(db).await.is_locked {
return Err(CoxHelpError::DetailsLocked);
}
if event.max_people == 0 {
return Err(CoxHelpError::CanceledEvent);
}
match sqlx::query!(
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
cox.id,
planned_event.id
event.id
)
.execute(db)
.await
@ -176,58 +191,77 @@ WHERE day=?
/// Cox decides to update own trip.
pub async fn update_own(
db: &SqlitePool,
cox: &CoxUser,
trip: &Trip,
max_people: i32,
notes: Option<&str>,
trip_type: Option<i64>, //TODO: Move to `TripType`
always_show: bool,
is_locked: bool,
update: &TripUpdate<'_>,
) -> Result<(), TripUpdateError> {
if !trip.is_trip_from_user(cox.id) {
if !update.trip.is_trip_from_user(update.cox.id) {
return Err(TripUpdateError::NotYourTrip);
}
let Some(trip_details_id) = trip.trip_details_id else {
let Some(trip_details_id) = update.trip.trip_details_id else {
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
};
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
let was_already_cancelled = tripdetails.max_people == 0;
sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
max_people,
notes,
trip_type,
always_show,
is_locked,
update.max_people,
update.notes,
update.trip_type,
update.always_show,
update.is_locked,
trip_details_id
)
.execute(db)
.await
.unwrap(); //Okay, as trip_details can only be created with proper DB backing
if max_people == 0 {
let rowers = TripWithUserAndType::from(db, trip.clone()).await.rower;
if update.max_people == 0 && !was_already_cancelled {
let rowers = TripWithUserAndType::from(db, update.trip.clone())
.await
.rower;
for user in rowers {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = if let Some(notes) = notes {
format!(": {notes}")
} else {
String::from(".")
let notes = match update.notes {
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
_ => String::from(""),
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt von {} am {} um {} wurde abgesagt{}",
cox.user.name, trip.day, trip.planned_starting_time, notes
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {}",
update.cox.user.name,
update.trip.day,
update.trip.planned_starting_time,
notes
),
"Absage Ausfahrt",
None,
Some(&format!(
"remove_user_trip_with_trip_details_id:{}",
trip_details_id
)),
)
.await;
}
}
} else {
Notification::delete_by_action(
db,
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
)
.await;
}
if update.max_people > 0 && was_already_cancelled {
Notification::delete_by_action(
db,
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
)
.await;
}
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
@ -246,16 +280,16 @@ WHERE day=?
pub async fn delete_by_planned_event(
db: &SqlitePool,
cox: &CoxUser,
planned_event: &PlannedEvent,
event: &Event,
) -> Result<(), TripHelpDeleteError> {
if planned_event.trip_details(db).await.is_locked {
if event.trip_details(db).await.is_locked {
return Err(TripHelpDeleteError::DetailsLocked);
}
let affected_rows = sqlx::query!(
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
cox.id,
planned_event.id
event.id
)
.execute(db)
.await
@ -314,6 +348,7 @@ pub enum CoxHelpError {
AlreadyRegisteredAsRower,
AlreadyRegisteredAsCox,
DetailsLocked,
CanceledEvent,
}
#[derive(Debug, PartialEq)]
@ -338,8 +373,8 @@ pub enum TripUpdateError {
mod test {
use crate::{
model::{
planned_event::PlannedEvent,
trip::TripDeleteError,
event::Event,
trip::{self, TripDeleteError},
tripdetails::TripDetails,
user::{CoxUser, User},
usertrip::UserTrip,
@ -389,7 +424,7 @@ mod test {
.await
.unwrap();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_ok());
}
@ -405,7 +440,7 @@ mod test {
.await
.unwrap();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_err());
@ -424,11 +459,17 @@ mod test {
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert!(
Trip::update_own(&pool, &cox, &trip, 10, None, None, false, false)
.await
.is_ok()
);
let update = trip::TripUpdate {
cox: &cox,
trip: &trip,
max_people: 10,
notes: None,
trip_type: None,
always_show: false,
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_ok());
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert_eq!(trip.max_people, 10);
@ -447,11 +488,16 @@ mod test {
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert!(
Trip::update_own(&pool, &cox, &trip, 10, None, Some(1), false, false)
.await
.is_ok()
);
let update = trip::TripUpdate {
cox: &cox,
trip: &trip,
max_people: 10,
notes: None,
trip_type: Some(1),
always_show: false,
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_ok());
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert_eq!(trip.max_people, 10);
@ -471,11 +517,16 @@ mod test {
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert!(
Trip::update_own(&pool, &cox, &trip, 10, None, None, false, false)
.await
.is_err()
);
let update = trip::TripUpdate {
cox: &cox,
trip: &trip,
max_people: 10,
notes: None,
trip_type: None,
always_show: false,
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_err());
assert_eq!(trip.max_people, 1);
}
@ -490,7 +541,7 @@ mod test {
.await
.unwrap();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();

View File

@ -7,6 +7,7 @@ use sqlx::{FromRow, SqlitePool};
use super::{
notification::Notification,
trip::{Trip, TripWithUserAndType},
triptype::TripType,
};
#[derive(FromRow, Debug, Serialize, Deserialize)]
@ -51,6 +52,13 @@ WHERE id like ?
.ok()
}
pub async fn triptype(&self, db: &SqlitePool) -> Option<TripType> {
match self.trip_type_id {
None => None,
Some(id) => TripType::find_by_id(db, id).await,
}
}
pub async fn find_by_startingdatetime(
db: &SqlitePool,
day: String,
@ -77,6 +85,12 @@ WHERE day = ? AND planned_starting_time = ?
return;
}
if self.max_people == 0 {
// Cox cancelled event, thus it's probably bad weather. Don't bother with sending
// notifications
return;
}
if Trip::find_by_trip_details(db, self.id).await.is_none() {
// This trip_details belongs to a planned_event, no need to do anything
return;
@ -120,7 +134,7 @@ WHERE day = ? AND planned_starting_time = ?
continue;
}
Notification::create(db, &user, &format!("Du hast dich als Ruderer bei der Ausfahrt von {} am {} um {} angemeldet. Bei allen Ausfahrten zu dieser Zeit sind nun alle Plätze ausgebucht. Damit noch mehr (Nicht-Steuerleute) mitfahren können, wäre es super, wenn du eine eigene Ausfahrt zur selben Zeit ausschreiben könntest.", cox.name, self.day, self.planned_starting_time), "Volle Ausfahrt", None).await;
Notification::create(db, &user, &format!("Du hast dich als Ruderer bei der Ausfahrt von {} am {} um {} angemeldet. Bei allen Ausfahrten zu dieser Zeit sind nun alle Plätze ausgebucht. Damit noch mehr (Nicht-Steuerleute) mitfahren können, wäre es super, wenn du eine eigene Ausfahrt zur selben Zeit ausschreiben könntest.", cox.name, self.day, self.planned_starting_time), "Volle Ausfahrt", None, None).await;
}
}
}

View File

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

View File

@ -14,7 +14,10 @@ use rocket::{
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{family::Family, log::Log, role::Role, tripdetails::TripDetails, Day};
use super::{
family::Family, log::Log, mail::Mail, notification::Notification, role::Role, stat::Stat,
tripdetails::TripDetails, Day,
};
use crate::tera::admin::user::UserEditForm;
const RENNRUDERBEITRAG: i32 = 11000;
@ -25,8 +28,9 @@ 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)]
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
pub struct User {
pub id: i64,
pub name: String,
@ -47,47 +51,25 @@ pub struct User {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserWithRolesAndNotificationCount {
pub struct UserWithDetails {
#[serde(flatten)]
pub user: User,
pub amount_unread_notifications: i32,
pub roles: Vec<String>,
}
impl UserWithRolesAndNotificationCount {
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
Self {
roles: user.roles(db).await,
amount_unread_notifications: user.amount_unread_notifications(db).await,
user,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserWithWaterStatus {
#[serde(flatten)]
pub user: User,
pub on_water: bool,
pub roles: Vec<String>,
}
impl UserWithWaterStatus {
impl UserWithDetails {
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
Self {
on_water: user.on_water(db).await,
roles: user.roles(db).await,
amount_unread_notifications: user.amount_unread_notifications(db).await,
user,
}
}
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
#[derive(Debug)]
pub enum LoginError {
InvalidAuthenticationCombo,
@ -98,7 +80,7 @@ pub enum LoginError {
NotACox,
NotATech,
GuestNotAllowed,
NoPasswordSet(User),
NoPasswordSet(Box<User>),
DeserializationError,
}
@ -159,6 +141,174 @@ impl Fee {
}
impl User {
pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(format!(
"Could not send welcome mail, because user {} has no email address",
self.name
));
};
if self.has_role(db, "Donau Linz").await {
self.send_welcome_mail_full_member(db, mail, smtp_pw)
.await?;
} else if self.has_role(db, "scheckbuch").await {
self.send_welcome_mail_scheckbuch(db, mail, smtp_pw).await?;
} else if self.has_role(db, "schnupperant").await {
self.send_welcome_mail_schnupper(db, mail, smtp_pw).await?;
} else {
return Err(format!(
"Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch or schnupperant group",
self.name
));
}
Log::create(
db,
format!("Willkommensemail wurde an {} versandt", self.name),
)
.await;
Ok(())
}
async fn send_welcome_mail_schnupper(
&self,
db: &SqlitePool,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
// 2 things to do:
// 1. Send mail to user
Mail::send_single(
db,
mail,
"Schnupperrudern beim ASKÖ Ruderverein Donau Linz",
format!(
"Hallo {0},
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
Liebe Grüße, Philipp", self.name),
smtp_pw,
).await?;
// 2. Notify all coxes
let coxes = Role::find_by_name(db, "schnupper-betreuer").await.unwrap();
Notification::create_for_role(
db,
&coxes,
&format!(
"Liebe Schnupper-Betreuer, {} nimmt am Schnupperkurs teil.",
self.name
),
"Neue(r) Schnupperteilnehmer:in ",
None,
None,
)
.await;
Ok(())
}
async fn send_welcome_mail_scheckbuch(
&self,
db: &SqlitePool,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
// 2 things to do:
// 1. Send mail to user
Mail::send_single(
db,
mail,
"ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich",
format!(
"Hallo {0},
herzlich willkommen beim ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dass Du Dich entschieden hast, das Rudern bei uns auszuprobieren. Mit Deinem Scheckbuch kannst Du jetzt an fünf Ausfahrten teilnehmen und so diesen Sport in seiner vollen Vielfalt erleben. Falls du die {1} € noch nicht bezahlt hast, nimm diese bitte zur nächsten Ausfahrt mit (oder überweise sie auf unser Bankkonto [dieses findest du auf https://rudernlinz.at]).
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge Dich bitte mit Deinem Namen ('{0}', ohne Anführungszeichen) ein. Beim ersten Mal kannst Du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst Du Dich jederzeit für eine Ausfahrt anmelden. Wir bieten mindestens einmal pro Woche Ausfahrten an, sowohl für Anfänger als auch für Fortgeschrittene (A+F Rudern). Zusätzliche Ausfahrten werden von unseren Steuerleuten ausgeschrieben, öfters reinschauen kann sich also lohnen :-)
Nach deinen 5 Ausfahrten würden wir uns freuen, dich als Mitglied in unserem Verein begrüßen zu dürfen.
Wir freuen uns darauf, Dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
smtp_pw,
).await?;
// 2. Notify all coxes
let coxes = Role::find_by_name(db, "cox").await.unwrap();
Notification::create_for_role(
db,
&coxes,
&format!(
"Liebe Steuerberechtigte, {} hat nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
self.name
),
"Neues Scheckbuch",
None,None
)
.await;
Ok(())
}
async fn send_welcome_mail_full_member(
&self,
db: &SqlitePool,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
// 2 things to do:
// 1. Send mail to user
Mail::send_single(
db,
mail,
"Willkommen im ASKÖ Ruderverein Donau Linz!",
format!(
"Hallo {0},
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.
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.
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.
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
Riemen- & Dollenbruch
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
// 2. Notify all coxes
let coxes = Role::find_by_name(db, "cox").await.unwrap();
Notification::create_for_role(
db,
&coxes,
&format!(
"Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}",
self.member_since_date.clone().unwrap(),
self.name
),
"Neues Vereinsmitglied",
None,
None,
)
.await;
Ok(())
}
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
if !self.has_role(db, "Donau Linz").await {
return None;
@ -267,6 +417,18 @@ impl User {
false
}
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)
.await
.unwrap()
{
Some(a) if a.is_empty() => false,
None => false,
_ => true,
}
}
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
sqlx::query!(
"SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;",
@ -326,12 +488,14 @@ WHERE id like ?
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
let name = name.trim().to_lowercase();
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user
WHERE name like ?
WHERE lower(name)=?
",
name
)
@ -456,6 +620,7 @@ ORDER BY last_access DESC
}
pub async fn create(db: &SqlitePool, name: &str) -> bool {
let name = name.trim();
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
.execute(db)
.await
@ -469,9 +634,7 @@ ORDER BY last_access DESC
family_id = Some(Family::insert(db).await)
}
let user_with_membershippdf = UserWithMembershipPdf::from(db, self.clone()).await;
if user_with_membershippdf.membership_pdf.is_none() {
if !self.has_membership_pdf(db).await {
if let Some(membership_pdf) = data.membership_pdf {
let mut stream = membership_pdf.open().await.unwrap();
let mut buffer = Vec::new();
@ -546,8 +709,8 @@ ORDER BY last_access DESC
}
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
let name = name.trim(); // just to make sure...
let Some(user) = User::find_by_name(db, name).await else {
let name = name.trim().to_lowercase(); // just to make sure...
let Some(user) = User::find_by_name(db, &name).await else {
if ![
"n-sageder",
"p-hofer",
@ -574,10 +737,11 @@ ORDER BY last_access DESC
"n.sageder",
"a.almousa",
"p.hofer",
"philipp-hofer",
"d.kortschak",
"[login]",
]
.contains(&name)
.contains(&name.as_str())
{
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
}
@ -603,7 +767,7 @@ ORDER BY last_access DESC
Err(LoginError::InvalidAuthenticationCombo)
} else {
info!("User {name} has no PW set");
Err(LoginError::NoPasswordSet(user))
Err(LoginError::NoPasswordSet(Box::new(user)))
}
}
@ -663,7 +827,7 @@ ORDER BY last_access DESC
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
if self.has_role(db, "scheckbuch").await {
let day = Day::new_guest(db, date, true).await;
if !day.planned_events.is_empty() {
if !day.events.is_empty() {
days.push(day);
}
} else {
@ -687,6 +851,19 @@ ORDER BY last_access DESC
6
}
}
pub(crate) async fn close_thousands_trip(&self, db: &SqlitePool) -> Option<String> {
let rowed_km = Stat::person(db, None, self).await.rowed_km;
if rowed_km % 1000 > 970 {
return Some(format!(
"{} braucht nur mehr {} km bis die {} km voll sind 🤑",
self.name,
1000 - rowed_km % 1000,
rowed_km + 1000 - (rowed_km % 1000)
));
}
None
}
}
#[async_trait]
@ -964,15 +1141,15 @@ impl<'r> FromRequest<'r> for VorstandUser {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PlannedEventUser(pub(crate) User);
pub struct EventUser(pub(crate) User);
impl From<PlannedEventUser> for User {
fn from(val: PlannedEventUser) -> Self {
impl From<EventUser> for User {
fn from(val: EventUser) -> Self {
val.0
}
}
impl Deref for PlannedEventUser {
impl Deref for EventUser {
type Target = User;
fn deref(&self) -> &Self::Target {
@ -989,16 +1166,7 @@ pub struct UserWithRolesAndMembershipPdf {
impl UserWithRolesAndMembershipPdf {
pub(crate) async fn from_user(db: &SqlitePool, user: User) -> Self {
let membership_pdf =
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", user.id)
.fetch_one(db)
.await
.unwrap()
{
Some(a) if a.is_empty() => false,
None => false,
_ => true,
};
let membership_pdf = user.has_membership_pdf(db).await;
Self {
roles: user.roles(db).await,
@ -1032,15 +1200,15 @@ impl UserWithMembershipPdf {
}
#[async_trait]
impl<'r> FromRequest<'r> for PlannedEventUser {
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, "planned_event").await {
Outcome::Success(PlannedEventUser(user))
if user.has_role(db, "manage_events").await {
Outcome::Success(EventUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}

View File

@ -76,6 +76,7 @@ impl UserTrip {
),
"Registrierung bei deiner Ausfahrt",
None,
None,
)
.await;
@ -149,7 +150,7 @@ pub enum UserTripDeleteError {
mod test {
use crate::{
model::{
planned_event::PlannedEvent, trip::Trip, tripdetails::TripDetails, user::CoxUser,
event::Event, trip::Trip, tripdetails::TripDetails, user::CoxUser,
usertrip::UserTripError,
},
testdb,
@ -239,8 +240,8 @@ mod test {
.await
.unwrap();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
let event = Event::find_by_id(&pool, 1).await.unwrap();
Trip::new_join(&pool, &cox, &event).await.unwrap();
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
let result = UserTrip::create(&pool, &cox, &trip_details, None)

View File

@ -17,6 +17,24 @@ pub struct Waterlevel {
pub tumittel: i64,
}
#[derive(Debug, Serialize)]
pub struct WaterlevelDay {
pub day: NaiveDate,
pub avg: i64,
pub fluctuation: i64,
}
pub struct Create {
pub day: NaiveDate,
pub time: String,
pub max: i64,
pub min: i64,
pub mittel: i64,
pub tumax: i64,
pub tumin: i64,
pub tumittel: i64,
}
impl Waterlevel {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id)
@ -31,20 +49,10 @@ impl Waterlevel {
.ok()
}
pub async fn create(
db: &mut Transaction<'_, Sqlite>,
day: NaiveDate,
time: String,
max: i64,
min: i64,
mittel: i64,
tumax: i64,
tumin: i64,
tumittel: i64,
) -> Result<(), String> {
pub async fn create(db: &mut Transaction<'_, Sqlite>, create: &Create) -> Result<(), String> {
sqlx::query!(
"INSERT INTO waterlevel(day, time, max, min, mittel, tumax, tumin, tumittel) VALUES (?,?,?,?,?,?,?,?)",
day, time, max, min, mittel, tumax, tumin, tumittel
create.day, create.time, create.max, create.min, create.mittel, create.tumax, create.tumin, create.tumittel
)
.execute(db.deref_mut())
.await
@ -52,15 +60,28 @@ impl Waterlevel {
Ok(())
}
pub async fn max_waterlevel_for_day(db: &SqlitePool, day: NaiveDate) -> Option<i64> {
sqlx::query!(
"SELECT MAX(mittel) as max FROM waterlevel WHERE day = ?",
pub async fn max_waterlevel_for_day(db: &SqlitePool, day: NaiveDate) -> Option<WaterlevelDay> {
let waterlevel = sqlx::query_as!(
Waterlevel,
"SELECT id, day, time, max, min, mittel, tumax, tumin, tumittel FROM waterlevel WHERE day = ? ORDER BY mittel DESC LIMIT 1",
day
)
.fetch_one(db)
.await
.unwrap()
.max
.fetch_optional(db)
.await.unwrap();
if let Some(waterlevel) = waterlevel {
let max_diff = (waterlevel.mittel - waterlevel.max).abs();
let min_diff = (waterlevel.mittel - waterlevel.min).abs();
let fluctuation = max_diff.max(min_diff);
return Some(WaterlevelDay {
day: waterlevel.day,
avg: waterlevel.mittel,
fluctuation,
});
}
None
}
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {

56
src/model/weather.rs Normal file
View File

@ -0,0 +1,56 @@
use std::ops::DerefMut;
use chrono::NaiveDate;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Weather {
pub id: i64,
pub day: NaiveDate,
pub max_temp: f64,
pub wind_gust: f64,
pub rain_mm: f64,
}
impl Weather {
pub async fn find_by_day(db: &SqlitePool, day: NaiveDate) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM weather WHERE day = ?", day)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, day: NaiveDate) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM weather WHERE day = ?", day)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn create(
db: &mut Transaction<'_, Sqlite>,
day: NaiveDate,
max_temp: f64,
wind_gust: f64,
rain_mm: f64,
) -> Result<(), String> {
sqlx::query!(
"INSERT INTO weather(day, max_temp, wind_gust, rain_mm) VALUES (?,?,?,?)",
day,
max_temp,
wind_gust,
rain_mm
)
.execute(db.deref_mut())
.await
.map_err(|e| e.to_string())?;
Ok(())
}
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {
sqlx::query!("DELETE FROM weather;")
.execute(db.deref_mut())
.await
.unwrap();
}
}

View File

@ -1,4 +1,5 @@
mod waterlevel;
mod weather;
use std::time::Duration;
@ -6,11 +7,15 @@ use job_scheduler_ng::{Job, JobScheduler};
use rocket::tokio::{self, task, time};
use sqlx::SqlitePool;
pub fn schedule(db: &SqlitePool) {
use crate::tera::Config;
pub fn schedule(db: &SqlitePool, config: &Config) {
let db = db.clone();
let openweathermap_key = config.openweathermap_key.clone();
tokio::task::spawn(async {
waterlevel::update(&db).await.unwrap();
weather::update(&db, &openweathermap_key).await.unwrap();
let mut sched = JobScheduler::new();
@ -22,6 +27,9 @@ pub fn schedule(db: &SqlitePool) {
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();
});
});
}));

View File

@ -2,7 +2,7 @@ use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use crate::model::waterlevel::Waterlevel;
use crate::model::waterlevel::{self, Waterlevel};
pub async fn update(db: &SqlitePool) -> Result<(), String> {
let mut tx = db.begin().await.unwrap();
@ -29,10 +29,18 @@ pub async fn update(db: &SqlitePool) -> Result<(), String> {
let time: NaiveTime = datetime.naive_utc().time();
let time_str = time.format("%H:%M").to_string();
Waterlevel::create(
&mut tx, date, time_str, max, min, mittel, tumax, tumin, tumittel,
)
.await?
let create = waterlevel::Create {
day: date,
time: time_str,
max,
min,
mittel,
tumax,
tumin,
tumittel,
};
Waterlevel::create(&mut tx, &create).await?
}
// 3. Save in DB
@ -77,25 +85,23 @@ fn fetch() -> Result<Station, String> {
if let Ok(data) = forecast {
if data.len() == 1 {
return Ok(data[0].clone());
Ok(data[0].clone())
} else {
return Err(format!(
Err(format!(
"Expected 1 station (Linz); got {} while fetching from {url}. Maybe the hydro data format changed?",
data.len()
));
))
}
} else {
return Err(format!(
Err(format!(
"Failed to parse the json received by {url}: {}",
forecast.err().unwrap()
));
))
}
}
Err(_) => {
return Err(format!(
"Could not fetch {url}, do you have internet? Maybe their server is down?"
));
}
Err(_) => Err(format!(
"Could not fetch {url}, do you have internet? Maybe their server is down?"
)),
}
}

118
src/scheduled/weather.rs Normal file
View File

@ -0,0 +1,118 @@
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use crate::model::weather::Weather;
pub async fn update(db: &SqlitePool, api_key: &str) -> Result<(), String> {
let mut tx = db.begin().await.unwrap();
// 1. Delete weather data
Weather::delete_all(&mut tx).await;
// 2. Fetch
let data = fetch(api_key)?;
for d in data.daily {
let Some(date) = DateTime::from_timestamp(d.dt, 0) else {
println!("Skipping {} because convertion to datetime failed", d.dt);
continue;
};
let max_temp = d.temp.max;
let wind_gust = d.wind_gust;
let rain_mm = d.rain.unwrap_or(0.);
Weather::create(
&mut tx,
date.naive_utc().into(),
max_temp,
wind_gust,
rain_mm,
)
.await?
}
// 3. Save in DB
tx.commit().await.unwrap();
Ok(())
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Data {
lat: f64,
lon: f64,
timezone: String,
timezone_offset: i64,
daily: Vec<Daily>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Daily {
dt: i64,
sunrise: i64,
sunset: i64,
moonrise: i64,
moonset: i64,
moon_phase: f64,
summary: String,
temp: Temp,
feels_like: FeelsLike,
pressure: i64,
humidity: i64,
dew_point: f64,
wind_speed: f64,
wind_deg: i64,
wind_gust: f64,
weather: Vec<DailyWeather>,
clouds: i64,
pop: f64,
rain: Option<f64>,
uvi: f64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Temp {
day: f64,
min: f64,
max: f64,
night: f64,
eve: f64,
morn: f64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct FeelsLike {
day: f64,
night: f64,
eve: f64,
morn: f64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct DailyWeather {
id: i64,
main: String,
description: String,
icon: String,
}
fn fetch(api_key: &str) -> Result<Data, String> {
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
match ureq::get(&url).call() {
Ok(response) => {
let data: Result<Data, _> = response.into_json();
if let Ok(data) = data {
Ok(data)
} else {
Err(format!(
"Failed to parse the json received by {url}: {}",
data.err().unwrap()
))
}
}
Err(_) => Err(format!(
"Could not fetch {url}, do you have internet? Maybe their server is down?"
)),
}
}

View File

@ -2,7 +2,7 @@ use crate::model::{
boat::{Boat, BoatToAdd, BoatToUpdate},
location::Location,
log::Log,
user::{AdminUser, User, UserWithRolesAndNotificationCount},
user::{AdminUser, User, UserWithDetails},
};
use rocket::{
form::Form,
@ -33,7 +33,7 @@ async fn index(
context.insert("users", &users);
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(admin.user, db).await,
&UserWithDetails::from_user(admin.user, db).await,
);
Template::render("admin/boat/index", context.into_json())

View File

@ -8,14 +8,14 @@ use serde::Serialize;
use sqlx::SqlitePool;
use crate::model::{
planned_event::PlannedEvent,
event::{self, Event},
tripdetails::{TripDetails, TripDetailsToAdd},
user::PlannedEventUser,
user::EventUser,
};
//TODO: add constraints (e.g. planned_amount_cox > 0)
#[derive(FromForm, Serialize)]
struct AddPlannedEventForm<'r> {
struct AddEventForm<'r> {
name: &'r str,
planned_amount_cox: i32,
tripdetails: TripDetailsToAdd<'r>,
@ -24,8 +24,8 @@ struct AddPlannedEventForm<'r> {
#[post("/planned-event", data = "<data>")]
async fn create(
db: &State<SqlitePool>,
data: Form<AddPlannedEventForm<'_>>,
_admin: PlannedEventUser,
data: Form<AddEventForm<'_>>,
_admin: EventUser,
) -> Flash<Redirect> {
let data = data.into_inner();
@ -34,14 +34,14 @@ async fn create(
//just created
//the object
PlannedEvent::create(db, data.name, data.planned_amount_cox, trip_details).await;
Event::create(db, data.name, data.planned_amount_cox, &trip_details).await;
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
}
//TODO: add constraints (e.g. planned_amount_cox > 0)
#[derive(FromForm)]
struct UpdatePlannedEventForm<'r> {
#[derive(FromForm, Debug)]
struct UpdateEventForm<'r> {
id: i64,
name: &'r str,
planned_amount_cox: i32,
@ -49,27 +49,27 @@ struct UpdatePlannedEventForm<'r> {
notes: Option<&'r str>,
always_show: bool,
is_locked: bool,
trip_type: Option<i64>,
}
#[put("/planned-event", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<UpdatePlannedEventForm<'_>>,
_admin: PlannedEventUser,
data: Form<UpdateEventForm<'_>>,
_admin: EventUser,
) -> Flash<Redirect> {
match PlannedEvent::find_by_id(db, data.id).await {
let update = event::EventUpdate {
name: data.name,
planned_amount_cox: data.planned_amount_cox,
max_people: data.max_people,
notes: data.notes,
always_show: data.always_show,
is_locked: data.is_locked,
trip_type_id: data.trip_type,
};
match Event::find_by_id(db, data.id).await {
Some(planned_event) => {
planned_event
.update(
db,
data.name,
data.planned_amount_cox,
data.max_people,
data.notes,
data.always_show,
data.is_locked,
)
.await;
planned_event.update(db, &update).await;
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
}
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
@ -77,13 +77,14 @@ async fn update(
}
#[get("/planned-event/<id>/delete")]
async fn delete(db: &State<SqlitePool>, id: i64, _admin: PlannedEventUser) -> Flash<Redirect> {
match PlannedEvent::find_by_id(db, id).await {
Some(planned_event) => {
planned_event.delete(db).await;
Flash::success(Redirect::to("/planned"), "Event gelöscht")
}
None => Flash::error(Redirect::to("/planned"), "PlannedEvent does not exist"),
async fn delete(db: &State<SqlitePool>, id: i64, _admin: EventUser) -> Flash<Redirect> {
let Some(event) = Event::find_by_id(db, id).await else {
return Flash::error(Redirect::to("/planned"), "Event does not exist");
};
match event.delete(db).await {
Ok(()) => Flash::success(Redirect::to("/planned"), "Event gelöscht"),
Err(e) => Flash::error(Redirect::to("/planned"), e),
}
}
@ -106,7 +107,7 @@ mod test {
fn test_delete() {
let db = testdb!();
let _ = PlannedEvent::find_by_id(&db, 1).await.unwrap();
let _ = Event::find_by_id(&db, 1).await.unwrap();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
@ -131,7 +132,7 @@ mod test {
assert_eq!(flash_cookie.value(), "7:successEvent gelöscht");
let event = PlannedEvent::find_by_id(&db, 1).await;
let event = Event::find_by_id(&db, 1).await;
assert_eq!(event, None);
}
@ -160,16 +161,16 @@ mod test {
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "5:errorPlannedEvent does not exist");
assert_eq!(flash_cookie.value(), "5:errorEvent does not exist");
let _ = PlannedEvent::find_by_id(&db, 1).await.unwrap();
let _ = Event::find_by_id(&db, 1).await.unwrap();
}
#[sqlx::test]
fn test_update() {
let db = testdb!();
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
let event = Event::find_by_id(&db, 1).await.unwrap();
assert_eq!(event.notes, Some("trip_details for a planned event".into()));
let rocket = rocket::build().manage(db.clone());
@ -201,7 +202,7 @@ mod test {
"7:successEvent erfolgreich bearbeitet"
);
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
let event = Event::find_by_id(&db, 1).await.unwrap();
assert_eq!(event.notes, Some("new-planned-event-text".into()));
}
@ -268,7 +269,7 @@ mod test {
assert_eq!(flash_cookie.value(), "7:successEvent hinzugefügt");
let event = PlannedEvent::find_by_id(&db, 2).await.unwrap();
let event = Event::find_by_id(&db, 2).await.unwrap();
assert_eq!(event.name, "my-cool-new-event");
}
}

View File

@ -10,7 +10,7 @@ use crate::model::log::Log;
use crate::model::mail::Mail;
use crate::model::role::Role;
use crate::model::user::AdminUser;
use crate::model::user::UserWithRolesAndNotificationCount;
use crate::model::user::UserWithDetails;
use crate::tera::Config;
#[get("/mail")]
@ -27,7 +27,7 @@ async fn index(
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(admin.user, db).await,
&UserWithDetails::from_user(admin.user, db).await,
);
context.insert("roles", &roles);

View File

@ -9,9 +9,9 @@ use crate::{
};
pub mod boat;
pub mod event;
pub mod mail;
pub mod notification;
pub mod planned_event;
pub mod schnupper;
pub mod user;
@ -80,7 +80,7 @@ pub fn routes() -> Vec<Route> {
ret.append(&mut boat::routes());
ret.append(&mut notification::routes());
ret.append(&mut mail::routes());
ret.append(&mut planned_event::routes());
ret.append(&mut event::routes());
ret.append(&mut routes![rss, show_rss, show_list, list]);
ret
}

View File

@ -2,8 +2,9 @@ use crate::model::{
log::Log,
notification::Notification,
role::Role,
user::{AdminUser, User, UserWithRolesAndNotificationCount},
user::{AdminUser, User, UserWithDetails},
};
use itertools::Itertools;
use rocket::{
form::Form,
get, post,
@ -26,24 +27,41 @@ async fn index(
}
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.user, db).await,
&UserWithDetails::from_user(user.user, db).await,
);
let users: Vec<User> = User::all(db)
.await
.into_iter()
.filter(|u| u.last_access.is_some()) // Not useful to send notifications to people who are
// not logging in
.sorted_by_key(|u| u.name.clone())
.collect();
context.insert("roles", &Role::all(db).await);
context.insert("users", &users);
Template::render("admin/notification", context.into_json())
}
#[derive(FromForm, Debug)]
pub struct NotificationToSend {
pub struct NotificationToSendGroup {
pub(crate) role_id: i32,
pub(crate) category: String,
pub(crate) message: String,
}
#[post("/notification", data = "<data>")]
async fn send(
#[derive(FromForm, Debug)]
pub struct NotificationToSendUser {
pub(crate) user_id: i32,
pub(crate) category: String,
pub(crate) message: String,
}
#[post("/notification/group", data = "<data>")]
async fn send_group(
db: &State<SqlitePool>,
data: Form<NotificationToSend>,
data: Form<NotificationToSendGroup>,
admin: AdminUser,
) -> Flash<Redirect> {
let d = data.into_inner();
@ -58,7 +76,7 @@ async fn send(
};
for user in User::all_with_role(db, &role).await {
Notification::create(db, &user, &d.message, &d.category, None).await;
Notification::create(db, &user, &d.message, &d.category, None, None).await;
}
Log::create(db, "Notification successfully sent".into()).await;
Flash::success(
@ -67,6 +85,32 @@ async fn send(
)
}
pub fn routes() -> Vec<Route> {
routes![index, send]
#[post("/notification/user", data = "<data>")]
async fn send_user(
db: &State<SqlitePool>,
data: Form<NotificationToSendUser>,
admin: AdminUser,
) -> Flash<Redirect> {
let d = data.into_inner();
Log::create(
db,
format!("{admin:?} trying to send this notification: {d:?}"),
)
.await;
let Some(user) = User::find_by_id(db, d.user_id).await else {
return Flash::error(Redirect::to("/admin/notification"), "User gibt's ned");
};
Notification::create(db, &user, &d.message, &d.category, None, None).await;
Log::create(db, "Notification successfully sent".into()).await;
Flash::success(
Redirect::to("/admin/notification"),
"Nachricht ausgeschickt",
)
}
pub fn routes() -> Vec<Route> {
routes![index, send_user, send_group]
}

View File

@ -1,32 +1,12 @@
use crate::model::{
role::Role,
user::{SchnupperBetreuerUser, User, UserWithRolesAndNotificationCount},
user::{SchnupperBetreuerUser, User, UserWithDetails},
};
use futures::future::join_all;
use rocket::{
get,
http::Status,
request::{FlashMessage, FromRequest, Outcome},
routes, Request, Route, State,
};
use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
// Custom request guard to extract the Referer header
struct Referer(String);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Referer {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.headers().get_one("Referer") {
Some(referer) => Outcome::Success(Referer(referer.to_string())),
None => Outcome::Error((Status::BadRequest, ())),
}
}
}
#[get("/schnupper")]
async fn index(
db: &State<SqlitePool>,
@ -38,9 +18,9 @@ async fn index(
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
.await
.into_iter()
.map(|u| async move { UserWithRolesAndNotificationCount::from_user(u, db).await })
.map(|u| async move { UserWithDetails::from_user(u, db).await })
.collect();
let users: Vec<UserWithRolesAndNotificationCount> = join_all(user_futures).await;
let users: Vec<UserWithDetails> = join_all(user_futures).await;
let mut context = Context::new();
if let Some(msg) = flash {
@ -49,7 +29,7 @@ async fn index(
context.insert("schnupperanten", &users);
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.into(), db).await,
);
Template::render("admin/schnupper/index", context.into_json())

View File

@ -1,14 +1,17 @@
use std::collections::HashMap;
use crate::model::{
family::Family,
log::Log,
logbook::Logbook,
role::Role,
user::{
AdminUser, User, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
UserWithRolesAndNotificationCount, VorstandUser,
use crate::{
model::{
family::Family,
log::Log,
logbook::Logbook,
role::Role,
user::{
AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
VorstandUser,
},
},
tera::Config,
};
use futures::future::join_all;
use rocket::{
@ -67,10 +70,7 @@ async fn index(
context.insert("users", &users);
context.insert("roles", &roles);
context.insert("families", &families);
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
Template::render("admin/user/index", context.into_json())
}
@ -102,10 +102,7 @@ async fn index_admin(
context.insert("users", &users);
context.insert("roles", &roles);
context.insert("families", &families);
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
Template::render("admin/user/index", context.into_json())
}
@ -133,7 +130,7 @@ async fn fees(
}
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(admin.into(), db).await,
&UserWithDetails::from_user(admin.into(), db).await,
);
Template::render("admin/user/fees", context.into_json())
@ -153,7 +150,7 @@ async fn scheckbuch(
for s in scheckbooks {
scheckbooks_with_roles.push((
Logbook::completed_with_user(db, &s).await,
UserWithRolesAndNotificationCount::from_user(s, db).await,
UserWithDetails::from_user(s, db).await,
))
}
@ -164,7 +161,7 @@ async fn scheckbuch(
}
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.into(), db).await,
);
Template::render("admin/user/scheckbuch", context.into_json())
@ -208,11 +205,36 @@ async fn fees_paid(
)
}
#[get("/user/<user>/send-welcome-mail")]
async fn send_welcome_mail(
db: &State<SqlitePool>,
_admin: AdminUser,
config: &State<Config>,
user: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, user).await else {
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
};
match user.send_welcome_email(db, &config.smtp_pw).await {
Ok(()) => Flash::success(
Redirect::to("/admin/user"),
format!("Willkommens-Email wurde an {} versandt.", user.name),
),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[get("/user/<user>/reset-pw")]
async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> {
async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await;
match user {
Some(user) => {
Log::create(
db,
format!("{} has resetted the pw for {}", admin.user.name, user.name),
)
.await;
user.reset_pw(db).await;
Flash::success(
Redirect::to("/admin/user"),
@ -338,6 +360,7 @@ pub fn routes() -> Vec<Route> {
fees,
fees_paid,
scheckbuch,
download_membership_pdf
download_membership_pdf,
send_welcome_mail
]
}

View File

@ -39,7 +39,6 @@ struct LoginForm<'r> {
password: &'r str,
}
#[derive(Debug)]
pub struct UserAgent(String);
#[rocket::async_trait]
@ -83,8 +82,8 @@ async fn login(
Log::create(
db,
format!(
"Succ login of {} with this useragent: {:?}",
login.name, agent
"Succ login of {} with this useragent: {}",
login.name, agent.0
),
)
.await;

View File

@ -1,7 +1,7 @@
use crate::model::{
boat::Boat,
boathouse::Boathouse,
user::{AdminUser, UserWithRolesAndNotificationCount, VorstandUser},
user::{AdminUser, UserWithDetails, VorstandUser},
};
use rocket::{
form::Form,
@ -39,7 +39,7 @@ async fn index(
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(admin.into(), db).await,
&UserWithDetails::from_user(admin.into(), db).await,
);
Template::render("board/boathouse", context.into_json())

View File

@ -13,7 +13,7 @@ use crate::{
model::{
boat::Boat,
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRolesAndNotificationCount},
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails},
},
tera::log::KioskCookie,
};
@ -59,7 +59,7 @@ async fn index(
context.insert("boats", &boats);
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.into(), db).await,
);
Template::render("boatdamages", context.into_json())

View File

@ -15,7 +15,7 @@ use crate::{
boat::Boat,
boatreservation::{BoatReservation, BoatReservationToAdd},
log::Log,
user::{DonauLinzUser, User, UserWithRolesAndNotificationCount},
user::{DonauLinzUser, User, UserWithDetails},
},
tera::log::KioskCookie,
};
@ -75,7 +75,7 @@ async fn index(
context.insert("user", &User::all(db).await);
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.into(), db).await,
);
Template::render("boatreservations", context.into_json())
@ -166,7 +166,7 @@ async fn update(
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
return Flash::error(
Redirect::to("/boatreservation"),
format!("Not allowed to update reservation (only admins + creator do so)."),
"Not allowed to update reservation (only admins + creator do so).".to_string(),
);
}

View File

@ -7,9 +7,9 @@ use rocket::{
use sqlx::SqlitePool;
use crate::model::{
event::Event,
log::Log,
planned_event::PlannedEvent,
trip::{CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
tripdetails::{TripDetails, TripDetailsToAdd},
user::CoxUser,
};
@ -54,18 +54,16 @@ async fn update(
cox: CoxUser,
) -> Flash<Redirect> {
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
match Trip::update_own(
db,
&cox,
&trip,
data.max_people,
data.notes,
data.trip_type,
data.always_show,
data.is_locked,
)
.await
{
let update = trip::TripUpdate {
cox: &cox,
trip: &trip,
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 {
Ok(_) => Flash::success(
Redirect::to("/planned"),
"Ausfahrt erfolgreich aktualisiert.",
@ -84,7 +82,7 @@ async fn update(
#[get("/join/<planned_event_id>")]
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
if let Some(planned_event) = PlannedEvent::find_by_id(db, planned_event_id).await {
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
match Trip::new_join(db, &cox, &planned_event).await {
Ok(_) => {
Log::create(
@ -97,6 +95,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
.await;
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
}
Err(CoxHelpError::CanceledEvent) => {
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
}
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
}
@ -136,7 +137,7 @@ async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flas
#[get("/remove/<planned_event_id>")]
async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
if let Some(planned_event) = PlannedEvent::find_by_id(db, planned_event_id).await {
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
match Trip::delete_by_planned_event(db, &cox, &planned_event).await {
Ok(_) => {
Log::create(

View File

@ -18,7 +18,7 @@ use tera::Context;
use crate::model::{
log::Log,
user::{AdminUser, User, UserWithRolesAndNotificationCount},
user::{AdminUser, User, UserWithDetails},
};
#[derive(Serialize)]
@ -51,7 +51,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
Template::render(
"ergo.final",
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(_user.user, db).await, thirty, dozen),
context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen),
)
}
@ -120,10 +120,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("users", &users);
context.insert("thirty", &thirty);
context.insert("dozen", &dozen);

View File

@ -24,12 +24,10 @@ use crate::model::{
LogbookUpdateError,
},
logtype::LogType,
user::{
AdminUser, DonauLinzUser, User, UserWithRolesAndNotificationCount, UserWithWaterStatus,
},
user::{AdminUser, DonauLinzUser, User, UserWithDetails},
};
pub struct KioskCookie(String);
pub struct KioskCookie(());
#[rocket::async_trait]
impl<'r> FromRequest<'r> for KioskCookie {
@ -37,7 +35,7 @@ impl<'r> FromRequest<'r> for KioskCookie {
async fn from_request(request: &'r Request<'_>) -> request::Outcome<KioskCookie, Self::Error> {
match request.cookies().get_private("kiosk") {
Some(cookie) => request::Outcome::Success(KioskCookie(cookie.value().to_string())),
Some(_) => request::Outcome::Success(KioskCookie(())),
None => request::Outcome::Forward(rocket::http::Status::SeeOther),
}
}
@ -51,23 +49,27 @@ async fn index(
) -> Template {
let boats = Boat::for_user(db, &user).await;
let mut coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
User::cox(db)
.await
.into_iter()
.map(|user| UserWithWaterStatus::from_user(user, db)),
.map(|user| UserWithDetails::from_user(user, db)),
)
.await;
coxes.retain(|u| u.roles.contains(&"Donau Linz".into()));
coxes.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let mut users: Vec<UserWithWaterStatus> = futures::future::join_all(
let mut users: Vec<UserWithDetails> = futures::future::join_all(
User::all(db)
.await
.into_iter()
.map(|user| UserWithWaterStatus::from_user(user, db)),
.map(|user| UserWithDetails::from_user(user, db)),
)
.await;
users.retain(|u| u.roles.contains(&"Donau Linz".into()));
users.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let logtypes = LogType::all(db).await;
let distances = Logbook::distances(db).await;
@ -89,7 +91,7 @@ async fn index(
context.insert("logtypes", &logtypes);
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
&UserWithDetails::from_user(user.into(), db).await,
);
context.insert("on_water", &on_water);
context.insert("distances", &distances);
@ -103,7 +105,7 @@ async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
Template::render(
"log.completed",
context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await),
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into(), db).await),
)
}
@ -113,7 +115,7 @@ async fn show_for_year(db: &State<SqlitePool>, user: AdminUser, year: i32) -> Te
Template::render(
"log.completed",
context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.user, db).await),
context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await),
)
}
@ -149,20 +151,30 @@ async fn kiosk(
_kiosk: KioskCookie,
) -> Template {
let boats = Boat::all(db).await;
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
User::cox(db)
.await
.into_iter()
.map(|user| UserWithWaterStatus::from_user(user, db)),
.map(|user| UserWithDetails::from_user(user, db)),
)
.await;
let users: Vec<UserWithWaterStatus> = futures::future::join_all(
coxes.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let mut users: Vec<UserWithDetails> = futures::future::join_all(
User::all(db)
.await
.into_iter()
.map(|user| UserWithWaterStatus::from_user(user, db)),
.map(|user| UserWithDetails::from_user(user, db)),
)
.await;
users.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let logtypes = LogType::all(db).await;
let distances = Logbook::distances(db).await;
@ -200,7 +212,7 @@ async fn create_logbook(
)
.await
{
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt erfolgreich hinzugefügt"),
Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),
@ -215,7 +227,7 @@ async fn create_logbook(
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten 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.")),
}
}
@ -250,7 +262,7 @@ async fn create_kiosk(
} else if let Some(shipmaster) = data.shipmaster {
User::find_by_id(db, shipmaster as i32).await.unwrap()
} else {
let Some(rower) = data.rowers.get(0) else {
let Some(rower) = data.rowers.first() else {
return Flash::error(
Redirect::to("/log"),
"Ausfahrt ohne Benutzer kann nicht angelegt werden.",
@ -288,6 +300,7 @@ async fn home_logbook(
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(e) => Flash::error(
Redirect::to("/log"),
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),

View File

@ -1,12 +1,12 @@
use rocket::{get, http::ContentType, routes, Route, State};
use sqlx::SqlitePool;
use crate::model::planned_event::PlannedEvent;
use crate::model::event::Event;
#[get("/cal")]
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
//TODO: add unit test once proper functionality is there
(ContentType::Calendar, PlannedEvent::get_ics_feed(db).await)
(ContentType::Calendar, Event::get_ics_feed(db).await)
}
pub fn routes() -> Vec<Route> {

View File

@ -21,9 +21,10 @@ use sqlx::SqlitePool;
use tera::Context;
use crate::model::{
logbook::Logbook,
notification::Notification,
role::Role,
user::{User, UserWithRolesAndNotificationCount},
user::{User, UserWithDetails, SCHECKBUCH},
};
pub(crate) mod admin;
@ -38,6 +39,7 @@ mod misc;
mod notification;
mod planned;
mod stat;
pub(crate) mod trailerreservation;
#[derive(FromForm, Debug)]
struct LoginForm<'r> {
@ -52,14 +54,29 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
context.insert("flash", &msg.into_inner());
}
if user.has_role(db, "scheckbuch").await {
let last_trips = Logbook::completed_with_user(db, &user).await;
context.insert("last_trips", &last_trips);
}
context.insert("notifications", &Notification::for_user(db, &user).await);
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("costs_scheckbuch", &SCHECKBUCH);
Template::render("index", context.into_json())
}
#[get("/impressum")]
async fn impressum(db: &State<SqlitePool>, user: Option<User>) -> Template {
let mut context = Context::new();
if let Some(user) = user {
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
}
Template::render("impressum", context.into_json())
}
#[get("/steering")]
async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let mut context = Context::new();
@ -78,10 +95,7 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
context.insert("coxes", &coxes);
context.insert("bootskundige", &bootskundige);
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
Template::render("steering", context.into_json())
}
@ -109,9 +123,7 @@ 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.")
}
struct Usage {
data: Vec<String>,
}
struct Usage {}
#[rocket::async_trait]
impl Fairing for Usage {
@ -174,11 +186,12 @@ pub struct Config {
rss_key: String,
smtp_pw: String,
usage_log_path: String,
pub openweathermap_key: String,
}
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
rocket
.mount("/", routes![index, steering])
.mount("/", routes![index, steering, impressum])
.mount("/auth", auth::routes())
.mount("/wikiauth", routes![wikiauth])
.mount("/log", log::routes())
@ -188,6 +201,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
.mount("/stat", stat::routes())
.mount("/boatdamage", boatdamage::routes())
.mount("/boatreservation", boatreservation::routes())
.mount("/trailerreservation", trailerreservation::routes())
.mount("/cox", cox::routes())
.mount("/admin", admin::routes())
.mount("/board", board::routes())
@ -196,7 +210,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
.register("/", catchers![unauthorized_error, forbidden_error])
.attach(Template::fairing())
.attach(AdHoc::config::<Config>())
.attach(Usage { data: Vec::new() })
.attach(Usage {})
}
#[cfg(test)]

View File

@ -10,10 +10,9 @@ use tera::Context;
use crate::model::{
log::Log,
logbook::Logbook,
tripdetails::TripDetails,
triptype::TripType,
user::{AllowedForPlannedTripsUser, User, UserWithRolesAndNotificationCount},
user::{AllowedForPlannedTripsUser, User, UserWithDetails},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
};
@ -27,16 +26,11 @@ async fn index(
let mut context = Context::new();
if user.has_role(db, "cox").await || user.has_role(db, "planned_event").await {
if user.has_role(db, "cox").await || user.has_role(db, "manage_events").await {
let triptypes = TripType::all(db).await;
context.insert("trip_types", &triptypes);
}
if user.has_role(db, "scheckbuch").await {
let last_trips = Logbook::completed_with_user(db, &user).await;
context.insert("last_trips", &last_trips);
}
let days = user.get_days(db).await;
if let Some(msg) = flash {
@ -44,10 +38,7 @@ async fn index(
}
context.insert("fee", &user.fee(db).await);
context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("days", &days);
Template::render("planned", context.into_json())
}

View File

@ -4,7 +4,7 @@ use sqlx::SqlitePool;
use crate::model::{
stat::{self, BoatStat, Stat},
user::{DonauLinzUser, UserWithRolesAndNotificationCount},
user::{DonauLinzUser, UserWithDetails},
};
use super::log::KioskCookie;
@ -16,7 +16,7 @@ async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
Template::render(
"stat.boats",
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, kiosk),
context!(loggedin_user: &UserWithDetails::from_user(user.into(), 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: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
)
}

View File

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

View File

@ -3,18 +3,32 @@
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full dark:text-white">
<h1 class="h1">Nachricht</h1>
<h1 class="h1">Nachricht senden</h1>
<div class="grid ">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Nachricht senden</h2>
<form action="/admin/notification" method="post" class="grid gap-3 p-3">
<h2 class="h2">Gruppe</h2>
<form action="/admin/notification/group"
method="post"
class="grid gap-3 p-3">
{{ macros::select(label="Gruppe", data=roles, name="role_id") }}
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }}
<input type="submit" class="btn btn-primary" value="Abschicken" />
</form>
</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">Person</h2>
<form action="/admin/notification/user"
method="post"
class="grid gap-3 p-3">
{{ macros::select(label="Person", data=users, name="user_id") }}
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }}
<input type="submit" class="btn btn-primary" value="Abschicken" />
</form>
</div>
</div>
</div>
{% endblock content %}

View File

@ -58,6 +58,10 @@
<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 %}

View File

@ -36,6 +36,7 @@
<a href="/stat/boats" class="px-2">Bootsauswertung</a>
<a href="/boatdamage" class="px-2">Bootsschaden</a>
<a href="/boatreservation" class="px-2">Bootsreservierung</a>
<a href="/trailerreservation" class="px-2">Hängerreservierung</a>
</div>
</header>
{% endif %}
@ -55,5 +56,3 @@
<script src="/public/main.js"></script>
</body>
</html>
</body>
</html>

View File

@ -81,7 +81,7 @@
{% endif %}
{% endif %}
{% if boatdamage.verified_at %}
<small class="block text-gray-600 dark:text-gray-100">Verifziert von {{ boatdamage.user_verified.name }} am/um {{ boatdamage.verified_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
<small class="block text-gray-600 dark:text-gray-100">Verifiziert von {{ boatdamage.user_verified.name }} am/um {{ boatdamage.verified_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
{% else %}
{% if loggedin_user and "tech" in loggedin_user.roles and boatdamage.fixed_at %}
<form action="/boatdamage/{{ boatdamage.id }}/verified"

View File

@ -0,0 +1,113 @@
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">Impressum</h1>
<div class="grid gap-3 my-5">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Allgemein</h2>
<div class="p-3">
Die Website wird vom ASKÖ Ruderverein Donau Linz betrieben.
<br />
<strong>Postanschrift:</strong>
<br />
ASKÖ Ruderverein Donau Linz
<br />
Heilhamerweg 2
<br />
4040 Linz
<br />
ZVR: 363903285
</div>
</div>
</div>
<div class="grid gap-3 my-5">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Datenschutz</h2>
<div class="p-3">
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.
</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.
</li>
</ul>
</div>
</div>
</div>
{% if loggedin_user %}
<div class="grid gap-3 my-5">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Daten</h2>
<div class="p-3">
Folgende personenbezogenen haben wir von dir gespeichert:
<ul>
<li>
<strong>Name:</strong> {{ loggedin_user.name }}
</li>
<li>
<strong>Passwort:</strong> (verschlüsselt als argon Hash)
</li>
<li>
<strong>Letzter Zugriff:</strong> {{ loggedin_user.last_access }}
</li>
<li>
<strong>Mitglied seit:</strong> {{ loggedin_user.member_since_date }}
</li>
<li>
<strong>Geburtsdatum:</strong> {{ loggedin_user.birthdate }}
</li>
<li>
<strong>Mail:</strong> {{ loggedin_user.mail }}
</li>
{% if loggedin_user.nickname %}
<li>
<strong>Spitzname:</strong> {{ loggedin_user.nickname }}
</li>
{% endif %}
<li>
<strong>Telefonnummer:</strong> {{ loggedin_user.phone }}
</li>
<li>
<strong>Adresse:</strong> {{ loggedin_user.address }}
</li>
<li>(Beitrittserklärung)</li>
{% if loggedin_user.family_id %}
<li>Verbindung zu Familienmitglied (gespeichert um Familientarif anstatt Vollmitglied zu haben)</li>
{% endif %}
<li>
<strong>Rollen:</strong> {{ loggedin_user.roles }} (werden für verschiedene Funktionen im Ruderassistenten verwendet)
</li>
<li>Anmeldungen zu Ausfahrten</li>
<li>Anmeldungen zu Events (zB Fetzenfahrt, Anrudern, USI-Rudern, ...)</li>
<li>Logbucheinträge</li>
<li>Selber eingetragene Bootsschäden, solange sie nicht > 1 Monat verifiziert und repariert wurden</li>
<li>Selber eingetragene Bootsreservierung</li>
<li>Boote, sofern es welche im Privatbesitz gibt</li>
</ul>
</div>
</div>
</div>
{% endif %}
<div class="grid gap-3 my-5">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Lizenzen</h2>
<div class="p-3">
<ul>
<li>
Die <strong>Wetterdaten</strong> werden von <a class="underline" href="https://openweathermap.org">OpenWeather</a> bereitgestellt.
</li>
<li>
<strong>Wasserstandsvorhersagen:</strong> Die Vorhersagen werden stündlich vom <a class="underline" href="https://hydro.ooe.gv.at">Hydrographischen Dienstes Oberösterreich</a> geladen und zwischengespeichert, der höchste Tages-Mittelwert wird gemeinsam mit der Schwankungsbreite bei den geplanten Ausfahrten angezeigt. Es handelt sich hierbei um ungeprüfte Rohdaten. Rohdatenfehler können durch betriebliche Störungen an den Messgeräten, Fernübertragungseinrichtungen u. dgl. entstehen. Die Vorhersagen sind daher mit Unsicherheiten behaftet! Mit der Länge des Vorhersagezeitraumeszeitraumes werden diese Unsicherheiten größer! Es wird keine Gewähr für die Vollständigkeit, Richtigkeit und Genauigkeit der dargestellten Daten übernommen. Gewährleistungs- und Haftungsansprüche werden ausdrücklich ausgeschlossen (sowohl vom Hydrographischen Dienstes Oberösterreich als auch vom ASKÖ Ruderverein Donau Linz).
</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -1,34 +1,39 @@
<footer class="bg-primary-950 dark:bg-primary-900 text-white w-full flex justify-center p-3">
<div class="max-w-screen-xl w-full flex justify-between items-center">
<div>
<span class="text-[#ff0000]">&hearts;</span>
Erstellt vom ASKÖ Ruderverein Donau Linz <a onclick="alert('Wir suchen kreative und motivierte Köpfe, die diesen Ruderassistenten mitgestalten möchten. Das Backend ist in Rust (Rocket), das Frontend in TypeScript und Teraform, wobei wir mit dem Gedanken spielen, zu Svelte(Kit) zu wechseln.\n\nWenn du 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.\n\nWirf den Anker bei uns ausi und melde dich bei Marie oder Philipp oder it@rudernlinz.at für eine Zukunft ohne optische Kenterung in Form von hässlichen Alerts ;)');"
style="text-decoration:underline">... und dir?</a>
<div class="max-w-screen-xl w-full">
<div class="w-full flex justify-between items-center">
<div>
<span class="text-[#ff0000]">&hearts;</span>
Erstellt vom ASKÖ Ruderverein Donau Linz <a class="underline"
onclick="alert('Wir suchen kreative und motivierte Köpfe, die diesen Ruderassistenten mitgestalten möchten. Das Backend ist in Rust (Rocket), das Frontend in TypeScript und Teraform, wobei wir mit dem Gedanken spielen, zu Svelte(Kit) zu wechseln.\n\nWenn du 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.\n\nWirf den Anker bei uns ausi und melde dich bei Marie oder Philipp oder it@rudernlinz.at für eine Zukunft ohne optische Kenterung in Form von hässlichen Alerts ;)');">... und dir?</a>
</div>
<div>
<button id="theme-toggle-js"
type="button"
data-theme="light"
class="btn btn-primary">
<span class="hidden dark:inline">
<svg xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
</svg>
</span>
<span class="inline dark:hidden">
<svg xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z" />
</svg>
</span>
</button>
</div>
</div>
<div>
<button id="theme-toggle-js"
type="button"
data-theme="light"
class="btn btn-primary">
<span class="hidden dark:inline">
<svg xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
</svg>
</span>
<span class="inline dark:hidden">
<svg xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z" />
</svg>
</span>
</button>
<div class="mt-3">
<a class="underline" href="/impressum">Impressum</a>
</div>
</div>
</footer>

View File

@ -70,7 +70,7 @@
</form>
{% endmacro new %}
{% macro boat_select(id="boat_id") %}
{{ macros::select(label="Boot", data=boats, name="boat_id", id=id, display=["name", " (","cat",")"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true, nonSelectableDefault=" -- Wähle ein Boot aus ---") }}
{{ macros::select(label="Boot", data=boats, name="boat_id", required=true, id=id, display=["name", " (","cat",")"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true, nonSelectableDefault=" -- Wähle ein Boot aus ---") }}
{% endmacro boat_select %}
{% macro rower_select(id, selected, amount_seats='', class='', init='false', cox_on_boat='', steering_person_id='') %}
{#{% if not amount_seats or amount_seats > 1 %}#}
@ -152,7 +152,7 @@
{% endfor %}
{% set amount_rowers = log.rowers | length %}
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
{% if amount_guests > 0 %}
{% if amount_guests > 0 and not log.boat.external %}
Gäste
<small class="text-gray-600">(ohne Account)</small>:
{{ amount_guests }}
@ -279,4 +279,8 @@
</details>
<input class="btn btn-primary" type="submit" value="Ausfahrt beenden" />
</form>
<a href="/log/{{ log.id }}/delete"
class="btn btn-alert w-full absolute bottom-0 left-0"
style="border-radius: 0"
onclick="return confirm('Willst du diesen Eintrag wirklich löschen? Die Daten gehen verloren');">Löschen</a>
{% endmacro home %}

View File

@ -78,6 +78,8 @@
class="block w-100 py-2 hover:text-primary-600 border-t">Bootsschaden</a>
<a href="/boatreservation"
class="block w-100 py-2 hover:text-primary-600 border-t">Bootsreservierung</a>
<a href="/trailerreservation"
class="block w-100 py-2 hover:text-primary-600 border-t">Hängerreservierung</a>
{% endif %}
{% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
<a href="/ergo" class="block w-100 py-2 hover:text-primary-600 border-t">Ergo</a>
@ -191,7 +193,7 @@
{% if rower.is_real_guest %}
<small class="text-gray-600 dark:text-gray-100">(Gast)</small>
{% if allow_removing %}
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}"
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name | urlencode }}"
class="absolute r-0 bg-red-500 w-5 h-5 text-white rounded-full flex items-center justify-center transform rotate-45 top-0 right-0">
<svg class="inline h-5 w-5"
width="16"

View File

@ -1,12 +1,9 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/log" as log %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">
Ruder
<wbr />
assistent
</h1>
<h1 class="h1">Ruder&shy;assistent</h1>
<div class="grid gap-3 my-5">
<div class="m-auto">
<a href="/planned"
@ -15,55 +12,57 @@
<span class="text-xl px-3">Geplante Ausfahrten</span>
</a>
</div>
<div id="notification"
class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Nachrichten</h2>
{% if loggedin_user.amount_unread_notifications > 10 %}
<div class="text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-center pb-3 px-3">
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte einen Moment Zeit sie zu überprüfen und als gelesen zu markieren (&#10003;).
</div>
{% endif %}
<div class="divide-y">
{% for notification in notifications %}
{% if not notification.read_at %}
<div class="relative flex justify-between items-center p-3">
<div class="grow me-4">
<small class="uppercase text-gray-600 dark:text-gray-100">
<strong>{{ notification.category }}</strong> &bullet; {{ notification.created_at | date(format="%d.%m.%Y %H:%M",) }}
</small>
<div class="mt-1">{{ notification.message | safe }}</div>
</div>
<div>
{% if not notification.read_at %}
<a href="/notification/{{ notification.id }}/read" class="inline-block">
<button class="btn btn-primary" type="button">
&#10003;
<span class="sr-only">Notification gelesen</span>
</button>
</a>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
<details class="py-3 border-t rounded-b-md">
<summary class="px-3 cursor-pointer">Vergangene Nachrichten (14 Tage)</summary>
<div class="divide-y text-sm">
{% if notifications %}
<div id="notification"
class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Nachrichten</h2>
{% if loggedin_user.amount_unread_notifications > 10 %}
<div class="text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-center pb-3 px-3">
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte einen Moment Zeit sie zu überprüfen und als gelesen zu markieren (&#10003;).
</div>
{% endif %}
<div class="divide-y">
{% for notification in notifications %}
{% if notification.read_at %}
<div class="p-3 relative">
<small class="uppercase text-gray-600 dark:text-gray-100">
<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 not notification.read_at %}
<div class="relative flex justify-between items-center p-3">
<div class="grow me-4">
<small class="uppercase text-gray-600 dark:text-gray-100">
<strong>{{ notification.category }}</strong> &bullet; {{ notification.created_at | date(format="%d.%m.%Y %H:%M",) }}
</small>
<div class="mt-1">{{ notification.message | safe }}</div>
</div>
<div>
{% if not notification.read_at %}
<a href="/notification/{{ notification.id }}/read" class="inline-block">
<button class="btn btn-primary" type="button">
&#10003;
<span class="sr-only">Notification gelesen</span>
</button>
</a>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</details>
</div>
<details class="py-3 border-t rounded-b-md">
<summary class="px-3 cursor-pointer">Vergangene Nachrichten (14 Tage)</summary>
<div class="divide-y text-sm">
{% for notification in notifications %}
{% if notification.read_at %}
<div class="p-3 relative">
<small class="uppercase text-gray-600 dark:text-gray-100">
<strong>{{ notification.category }}</strong> &bullet; {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }}
</small>
<div class="mt-1">{{ notification.message | safe }}</div>
</div>
{% endif %}
{% endfor %}
</div>
</details>
</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 %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
@ -91,6 +90,10 @@
<a href="/boatreservation"
class="block w-100 py-2 hover:text-primary-600">Bootsreservierung</a>
</li>
<li class="py-1">
<a href="/trailerreservation"
class="block w-100 py-2 hover:text-primary-600">Hängerreservierung</a>
</li>
<li class="py-1">
<a href="/steering" class="block w-100 py-2 hover:text-primary-600">Steuerleute & Co</a>
</li>
@ -98,14 +101,26 @@
</div>
{% endif %}
{% if "scheckbuch" in loggedin_user.roles %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Scheckbuch</h2>
<ul class="list-none ms-2 divide-y divide-gray-200 dark:divide-primary-600">
<li class="py-1">
<a href="/planned" class="block w-100 py-2 hover:text-primary-600">Geplante Ausfahrten</a>
</li>
</ul>
<div class="grid gap-3 mb-4">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Scheckbuch</h2>
{% if "paid" not in loggedin_user.roles %}
<div class="p-3 dark:text-white bg-white dark:bg-primary-900">
Bitte nimm zur nächsten Ausfahrt die {{ costs_scheckbuch / 100 }}&nbsp;€ für das Scheckbuch mit. Falls du das bereits gemacht hast, gibt uns bitte kurz Bescheid, dass dies noch nicht eingetragen wurde.
</div>
{% endif %}
<div class="text-sm p-3 bg-gray-200 bg-opacity-80 dark:bg-primary-950 dark:text-white text-primary-950">
<h3>Du hast bisher an {{ last_trips | length }} deiner 5 Scheckbuch-Ausfahrten teilgenommen.</h3>
{% if last_trips %}
<ol class="mt-3">
{% for last_trip in last_trips %}
<li>{{ log::show_old(log=last_trip, state="completed", only_ones=false, index=loop.index) }}</li>
{% endfor %}
</ol>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if "schnupper-betreuer" in loggedin_user.roles %}

View File

@ -3,22 +3,6 @@
{% extends "base" %}
{% block content %}
<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{% if "scheckbuch" in loggedin_user.roles %}
<div class="grid gap-3 sm:col-span-2 lg:col-span-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Scheckbuch</h2>
<div class="text-sm p-3">
<h3>Du hast bisher {{ last_trips | length }} deiner 5 Scheckbuch-Ausfahrten gemacht:</h3>
<ol>
{% for last_trip in last_trips %}
<li>{{ log::show_old(log=last_trip, state="completed", only_ones=false, index=loop.index) }}</li>
{% endfor %}
</ol>
</div>
</div>
</div>
{% endif %}
{% if "paid" not in loggedin_user.roles and "Donau Linz" in loggedin_user.roles %}
<div class="grid gap-3 sm:col-span-2 lg:col-span-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
@ -74,11 +58,11 @@
<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1>
{% include "includes/buttons" %}
{% for day in days %}
{% set amount_trips = day.planned_events | length + day.trips | length %}
{% set amount_trips = day.events | length + day.trips | length %}
{% set_global day_cox_needed = false %}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events %}
{% if planned_event.cox_needed %}
{% if day.events | length > 0 %}
{% for event in day.events %}
{% if event.cox_needed %}
{% set_global day_cox_needed = true %}
{% endif %}
{% endfor %}
@ -92,73 +76,94 @@
<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">
{{ day.day| date(format="%d.%m.%Y") }}
<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}
{% if day.max_waterlevel %}&bullet; <a href="https://hydro.ooe.gv.at/#/overview/Wasserstand/station/16668/Linz/Wasserstand" target="_blank" title="Prognostizierter maximaler Wasserstand am {{ day.day | date(format="%A", locale="de_AT") }}: {{ day.max_waterlevel }} cm">🌊{{ day.max_waterlevel }} cm</a>{% endif %}
{% if day.max_waterlevel %}
&bullet; <a href="https://hydro.ooe.gv.at/#/overview/Wasserstand/station/16668/Linz/Wasserstand"
target="_blank"
title="Prognostizierter maximaler Wasserstand am {{ day.day | date(format="%A", locale="de_AT") }}: {{ day.max_waterlevel.avg }} ± {{ day.max_waterlevel.fluctuation }} cm (ungeprüfte Rohdaten, für Details siehe die Infos dazu im Impressum)">🌊{{ day.max_waterlevel.avg }} ± {{ day.max_waterlevel.fluctuation }} cm</a>
{% endif %}
</small>
{% if day.weather %}
<small class="inline-block text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">
Temp:&nbsp;{{ day.weather.max_temp | round }}° &bullet; Windböe:&nbsp;{{ day.weather.wind_gust | round }}&nbsp;km/h &bullet; Regen:&nbsp;{{ day.weather.rain_mm | round }}&nbsp;mm
</small>
{% endif %}
</h2>
{% if day.planned_events | length > 0 or day.trips | length > 0 %}
{% if day.events | length > 0 or day.trips | length > 0 %}
<div class="grid grid-cols-1 gap-3 mb-3">
{# --- START Events --- #}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %}
{% set amount_cur_cox = planned_event.cox | length %}
{% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %}
{% if day.events | length > 0 %}
{% for event in day.events | sort(attribute="planned_starting_time") %}
{% set amount_cur_cox = event.cox | length %}
{% set amount_cox_missing = event.planned_amount_cox - amount_cur_cox %}
<div class="pt-2 px-3 border-t border-gray-200"
style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}">
style="order: {{ event.planned_starting_time | replace(from=":", to="") }}">
<div class="flex justify-between items-center">
<div class="mr-1">
<strong class="text-primary-900 dark:text-white">
{{ planned_event.planned_starting_time }}
Uhr
</strong>
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}
{%- if planned_event.trip_type %}
- {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}
{%- endif -%}
)</small>
{% if event.max_people == 0 %}
<strong class="text-[#f43f5e]">&#9888; Absage
{{ event.planned_starting_time }}
Uhr
</strong>
<small class="text-[#f43f5e]">({{ event.name }}
{%- if event.trip_type %}
- {{ event.trip_type.icon | safe }}&nbsp;{{ event.trip_type.name }}
{%- endif -%}
)</small>
{% else %}
<strong class="text-primary-900 dark:text-white">
{{ event.planned_starting_time }}
Uhr
</strong>
<small class="text-gray-600 dark:text-gray-100">({{ event.name }}
{%- if event.trip_type %}
- {{ event.trip_type.icon | safe }}&nbsp;{{ event.trip_type.name }}
{%- endif -%}
)</small>
{% endif %}
<br />
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }})
{% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}
{% if planned_event.notes %}<small class='block'>{{ planned_event.notes }}</small>{% endif %}
" data-body="#event{{ planned_event.trip_details_id }}" class="inline-block link-primary mr-3">
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ event.planned_starting_time }} Uhr</strong> ({{ event.name }})
{% if event.trip_type %}<small class='block'>{{ event.trip_type.desc }}</small>{% endif %}
{% if event.notes %}<small class='block'>{{ event.notes }}</small>{% endif %}
" data-body="#event{{ event.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
<div class="text-right grid gap-2">
{# --- START Row Buttons --- #}
{% set_global cur_user_participates = false %}
{% for rower in planned_event.rower %}
{% for rower in event.rower %}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/planned/remove/{{ planned_event.trip_details_id }}"
<a href="/planned/remove/{{ event.trip_details_id }}"
class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if planned_event.max_people > planned_event.rower | length and cur_user_participates == false %}
<a href="/planned/join/{{ planned_event.trip_details_id }}"
{% if event.max_people > event.rower | length and cur_user_participates == false %}
<a href="/planned/join/{{ event.trip_details_id }}"
class="btn btn-primary btn-fw"
{% if planned_event.trip_type %}onclick="return confirm('{{ planned_event.trip_type.question }}');"{% endif %}>Mitrudern</a>
{% if event.trip_type %}onclick="return confirm('{{ event.trip_type.question }}');"{% endif %}>Mitrudern</a>
{% endif %}
{# --- END Row Buttons --- #}
{# --- START Cox Buttons --- #}
{% if "cox" in loggedin_user.roles %}
{% set_global cur_user_participates = false %}
{% for cox in planned_event.cox %}
{% for cox in event.cox %}
{% if cox.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/cox/remove/{{ planned_event.id }}"
<a href="/cox/remove/{{ event.id }}"
class="block btn btn-attention btn-fw">
{% include "includes/cox-icon" %}
Abmelden
</a>
{% elif planned_event.planned_amount_cox > 0 %}
<a href="/cox/join/{{ planned_event.id }}"
{% elif event.planned_amount_cox > 0 %}
<a href="/cox/join/{{ event.id }}"
class="block btn {% if amount_cox_missing > 0 %} btn-dark {% else %} btn-gray {% endif %} btn-fw"
{% if planned_event.trip_type %}onclick="return confirm('{{ planned_event.trip_type.question }}');"{% endif %}>
{% if event.trip_type %}onclick="return confirm('{{ event.trip_type.question }}');"{% endif %}>
{% include "includes/cox-icon" %}
Steuern
</a>
@ -169,59 +174,88 @@
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div id="event{{ planned_event.trip_details_id }}">
<div id="event{{ event.trip_details_id }}">
{# --- START List Coxes --- #}
{% if planned_event.planned_amount_cox > 0 %}
{% if amount_cox_missing > 0 %}
{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
{% if event.planned_amount_cox > 0 %}
{% if event.max_people == 0 %}
{{ macros::box(participants=event.cox, empty_seats="", header='Absage', bg='[#f43f5e]') }}
{% else %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% if amount_cox_missing > 0 %}
{{ macros::box(participants=event.cox, empty_seats=event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
{% else %}
{{ macros::box(participants=event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% endif %}
{% endif %}
{% endif %}
{# --- END List Coxes --- #}
{# --- START List Rowers --- #}
{% if planned_event.max_people > 0 %}
{% set amount_cur_rower = planned_event.rower | length %}
{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="planned_event" in loggedin_user.roles) }}
{% set amount_cur_rower = event.rower | length %}
{% if event.max_people == 0 %}
{{ macros::box(header='Absage', bg='[#f43f5e]', participants=event.rower, trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
{% else %}
{{ macros::box(participants=event.rower, empty_seats=event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
{% endif %}
{# --- END List Rowers --- #}
{% if "planned_event" in loggedin_user.roles %}
<form action="/planned/join/{{ planned_event.trip_details_id }}"
method="get" />
{% if "manage_events" in loggedin_user.roles %}
<form action="/planned/join/{{ event.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen"
class="btn btn-primary w-full rounded-t-none-important"
type="submit" />
</form>
{% endif %}
{% if planned_event.allow_guests %}
{% if event.allow_guests %}
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
{% endif %}
{% if "planned_event" in loggedin_user.roles %}
{% if "manage_events" in loggedin_user.roles %}
{# --- START Edit Form --- #}
<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">Ausfahrt bearbeiten</h3>
<form action="/admin/planned-event" method="post" class="grid gap-3">
<input type="hidden" name="_method" value="put" />
<input type="hidden" name="id" value="{{ planned_event.id }}" />
{{ macros::input(label='Titel', name='name', type='input', value=planned_event.name) }}
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='0') }}
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=planned_event.planned_amount_cox, required=true, min='0') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=planned_event.id,checked=planned_event.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }}
<input type="hidden" name="id" value="{{ event.id }}" />
{{ macros::input(label='Titel', name='name', type='input', value=event.name) }}
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='1') }}
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=event.planned_amount_cox, required=true, min='0') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=event.id,checked=event.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=event.id,checked=event.is_locked) }}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=event.trip_type_id) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=event.notes) }}
<input value="Speichern" class="btn btn-primary" type="submit" />
</form>
</div>
{# --- END Edit Form --- #}
{# --- START Delete Btn --- #}
<div class="text-right">
<a href="/admin/planned-event/{{ planned_event.id }}/delete"
class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% if event.rower | length == 0 and amount_cur_cox == 0 %}
<div class="text-right mt-6">
<a href="/admin/planned-event/{{ event.id }}/delete"
class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% else %}
{% if event.max_people == 0 %}
Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
{% else %}
<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">Event absagen</h3>
<form action="/admin/planned-event" method="post" class="grid">
<input type="hidden" name="_method" value="put" />
<input type="hidden" name="id" value="{{ event.id }}" />
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
{{ macros::input(label='', name='name', type='hidden', value=event.name) }}
{{ macros::input(label='', name='max_people', type='hidden', value=event.max_people) }}
{{ macros::input(label='', name='planned_amount_cox', type='hidden', value=event.planned_amount_cox) }}
{{ macros::input(label='', name='always_show', type='hidden', value=event.always_show) }}
{{ macros::input(label='', name='is_locked', type='hidden', value=event.is_locked) }}
{{ macros::input(label='', name='trip_type', type='hidden', value=event.trip_type_id) }}
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
</form>
</div>
{% endif %}
{% endif %}
{% endif %}
{# --- END Delete Btn --- #}
</div>
@ -247,16 +281,16 @@
{{ trip.cox_name -}}
{% if trip.trip_type %}
-
{{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }}
{% endif -%}
{{ trip.trip_type.icon | safe }}&nbsp;{{ trip.trip_type.name }}
{%- endif -%}
)</small>
{% else %}
<strong class="text-primary-900 dark:text-white">{{ trip.planned_starting_time }}
Uhr</strong>
<small class="text-gray-600 dark:text-gray-100">({{ trip.cox_name -}}
{% if trip.trip_type %}
- {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}
{% endif -%}
- {{ trip.trip_type.icon | safe }}&nbsp;{{ trip.trip_type.name }}
{%- endif -%}
)</small>
{% endif %}
<br />
@ -334,7 +368,7 @@
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt absagen</h3>
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid">
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
{{ macros::input(label='', name='notes', type='hidden', value=trip.notes) }}
{{ 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) }}
@ -356,9 +390,9 @@
{% endif %}
</div>
{# --- START Add Buttons --- #}
{% if "planned_event" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "planned_event" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
{% if "planned_event" in loggedin_user.roles %}
{% 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">
{% if "manage_events" in loggedin_user.roles %}
<a href="#"
data-sidebar="true"
data-trigger="sidebar"
@ -372,7 +406,7 @@
{% endif %}
{% if "cox" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200
{% if "planned_event" in loggedin_user.roles %}
{% if "manage_events" in loggedin_user.roles %}
rounded-br-md
{% else %}
rounded-b-md
@ -392,7 +426,7 @@
{% if "cox" in loggedin_user.roles %}
{% include "forms/trip" %}
{% endif %}
{% if "planned_event" in loggedin_user.roles %}
{% if "manage_events" in loggedin_user.roles %}
{% include "forms/event" %}
{% endif %}
{% endblock content %}

View File

@ -0,0 +1,98 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/log" as log %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">Hängerreservierungen</h1>
<h2 class="text-md font-bold tracking-wide bg-primary-900 mt-3 p-3 text-white flex justify-between items-center rounded-md">
Neue Reservierung
<a href="#"
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
data-sidebar="true"
data-trigger="sidebar"
data-header="Neue Reservierung anlegen"
data-body="#new-reservation">
{% include "includes/plus-icon" %}
<span class="sr-only">Neue Reservierung eintragen</span>
</a>
</h2>
<div class="hidden">
<div id="new-reservation">
<form action="/trailerreservation/new" method="post" class="grid gap-3">
{{ macros::select(label="Anhänger", data=trailers, name="trailer_id", id="trailer_id", display=["name"], wrapper_class="col-span-4", nonSelectableDefault=" -- Wähle einen Hänger aus ---", required=true) }}
{% if not loggedin_user %}{{ macros::select(label='Reserviert von', data=user, name='user_id_applicant') }}{% endif %}
{{ macros::input(label='Beginn', name='start_date', type='date', required=true, wrapper_class='col-span-4') }}
{{ macros::input(label='Ende', name='end_date', type='date', required=true, wrapper_class='col-span-4') }}
{{ macros::input(label='Uhrzeit (zB ab 14:00 Uhr, ganztägig, ...)', name='time_desc', type='text', required=true, wrapper_class='col-span-4') }}
{{ macros::input(label='Zweck (Wanderfahrt, ...)', name='usage', type='text', required=true, wrapper_class='col-span-4') }}
<input type="submit"
class="btn btn-primary w-full col-span-4"
value="Reservierung eintragen" />
</form>
</div>
</div>
<div class="search-wrapper">
<label for="name" class="sr-only">Suche</label>
<input type="search"
name="name"
id="filter-js"
class="search-bar"
placeholder="Suchen nach Namen...">
</div>
<div id="filter-result-js" class="search-result"></div>
{% for reservation in trailerreservations %}
{% set allowed_to_edit = false %}
{% if loggedin_user %}
{% if loggedin_user.id == reservation.user_applicant.id or "admin" in loggedin_user.roles %}
{% set allowed_to_edit = true %}
{% endif %}
{% endif %}
<div data-filterable="true"
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>
{{ reservation.trailer.name }}
<br />
<strong>Reservierung:</strong>
{{ reservation.user_applicant.name }}
<br />
<strong>Datum:</strong>
{{ reservation.start_date }}
{% if reservation.end_date != reservation.start_date %}
-
{{ reservation.end_date }}
{% endif %}
<br />
{% if not allowed_to_edit %}
<strong>Uhrzeit:</strong>
{{ reservation.time_desc }}
<br />
<strong>Zweck:</strong>
{{ reservation.usage }}
{% endif %}
{% if allowed_to_edit %}
<form action="/trailerreservation"
method="post"
class="bg-white dark:bg-primary-900 pt-3 rounded-md w-full">
<div class="w-full grid gap-3">
<input type="hidden" name="id" value="{{ reservation.id }}" />
{{ macros::input(label='Uhrzeit', name='time_desc', id=loop.index, type="text", value=reservation.time_desc, readonly=false) }}
{{ macros::input(label='Zweck', name='usage', id=loop.index, type="text", value=reservation.usage, readonly=false) }}
</div>
<div class="mt-3 text-right">
<a href="/trailerreservation/{{ reservation.id }}/delete"
class="w-28 btn btn-alert"
onclick="return confirm('Willst du diese Reservierung wirklich löschen?');">
{% include "includes/delete-icon" %}
Löschen
</a>
<input value="Ändern" type="submit" class="w-28 btn btn-primary ml-1" />
</div>
</form>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endblock content %}