Compare commits

..

160 Commits

Author SHA1 Message Date
0e4ffce9e8 chore: fix merge 2024-09-16 13:52:44 +02:00
c946a0e2c1 Merge branch 'master' into stundenplan 2024-09-16 03:39:28 +02:00
Sarah Vaupel
8ad6d5164c refactor(schedule): rename auxiliray def, more doc 2021-05-06 14:57:06 +02:00
Sarah Vaupel
8d06a035b1 refactor(schedule): slightly cleanup imports, add documentation 2021-05-06 14:49:58 +02:00
Sarah Vaupel
e4ba4414c6 refactor(schedule): move SlotAssociation, move weekDays 2021-05-06 14:35:34 +02:00
Sarah Vaupel
0c8de277d5 refactor(schedule): update comments and TODOs, move fetchActiveTerms 2021-05-06 14:03:51 +02:00
Sarah Vaupel
4e6ffa7b85 style(schedule): highlight running schedule entries 2021-05-06 13:41:05 +02:00
Sarah Vaupel
9f954061e3 style(schedule): highlight running schedule entries 2021-05-06 12:21:56 +02:00
Sarah Vaupel
4a726f09fb refactor(schedule): enhance course schedule opt toggle 2021-05-06 10:35:40 +02:00
Sarah Vaupel
a4a26afa7a style(schedule): enhance display for joined entries 2021-05-06 09:45:54 +02:00
Sarah Vaupel
bbd5b73142 fix(schedule): remove typo in template 2021-05-05 16:03:41 +02:00
Sarah Vaupel
f178737f78 style(schedule): override link color in schedule entries 2021-05-05 16:01:26 +02:00
Sarah Vaupel
c1b94dbb53 fix(schedule): fix display of unset and hidden room info 2021-05-05 14:44:38 +02:00
Sarah Vaupel
6b8a140aca fix(schedule): account for showRoom 2021-05-05 11:26:54 +02:00
Sarah Vaupel
f46f23785d Merge branch 'master' into stundenplan 2021-05-04 18:30:59 +02:00
Gregor Kleen
99475ed253 feat(schedule): highlight current day/timeslot 2020-11-12 13:07:12 +01:00
Gregor Kleen
7733bd6280 fix(schedule): consider lecture period & holidays 2020-11-12 12:53:00 +01:00
Gregor Kleen
5bc25d1d3f feat(schedule): continue events over multiple slots
Also fix handling of ExceptNoOccur
2020-11-12 11:42:37 +01:00
Gregor Kleen
4bc1a8eac0 fix(schedule): use local day instead of utc 2020-11-11 18:05:57 +01:00
Gregor Kleen
dba0891000 fix(schedule): improve offset behaviour 2020-11-11 16:24:36 +01:00
Gregor Kleen
ff9916fde6 refactor(schedule): (type) cleanup 2020-11-11 14:04:54 +01:00
Gregor Kleen
22f43a9631 Merge branch 'master' into stundenplan 2020-11-11 10:15:27 +01:00
Sarah Vaupel
265d5f3ddd fix(schedule): use should-be-displayed result for exam occurrence actions 2020-11-10 21:24:00 +01:00
Sarah Vaupel
3919152ede fix(schedule): use should-be-displayed result for tutorial actions 2020-11-10 20:51:25 +01:00
Sarah Vaupel
601cb3179f fix(schedule): fix should-be-displayed result 2020-11-10 20:43:48 +01:00
Sarah Vaupel
ec04fe161e fix(schedule): use should-be-displayed result for course event actions 2020-11-10 20:31:45 +01:00
Sarah Vaupel
45a5766210 refactor(schedule): rename schedule-related fetch functions, export should-be-displayed 2020-11-10 20:28:08 +01:00
Sarah Vaupel
3589831541 chore(schedule): enhance messages 2020-11-09 21:30:38 +01:00
Sarah Vaupel
62c8296c6a Merge branch 'master' into stundenplan 2020-11-09 20:59:11 +01:00
Sarah Vaupel
4282af893d fix(schedule): treat exam occurrences independently from course schedule opts 2020-11-09 20:42:07 +01:00
Sarah Vaupel
b0023dfa67 fix(schedule-opt): course schedule opt does not affect unregistered tutorials 2020-11-09 19:31:14 +01:00
Sarah Vaupel
1d34cae4e2 fix(schedule): make course schedule opt-ins work for unregistered users 2020-11-09 19:27:20 +01:00
Sarah Vaupel
42c133d3ed fix(schedule-opt): account for registration status in course schedule opt 2020-11-09 19:00:22 +01:00
Sarah Vaupel
d8a921f335 fix(schedule-opt): account for course schedule opt in tutorial opt 2020-11-09 18:51:06 +01:00
Sarah Vaupel
cd450848a4 fix(schedule-opt): account for course schedule opt in course event opt 2020-11-09 18:42:26 +01:00
Sarah Vaupel
374cb6250d fix(schedule-opt): account for course schedule opt in tutorial actions 2020-11-09 17:59:51 +01:00
Sarah Vaupel
4199cc624b fix(schedule): account for isRegistered for tutorials schedule opt 2020-11-09 13:09:07 +01:00
Sarah Vaupel
2c9d5e0a22 fix(schedule): hide course event reset action if no opt to reset 2020-11-09 12:59:27 +01:00
Sarah Vaupel
c6a84b314c feat(schedule): account for course schedule opt in fetches 2020-11-09 12:56:28 +01:00
Sarah Vaupel
bab72a5e2e feat(schedule-opts): add course schedule opt actions to CShowR 2020-11-09 11:30:53 +01:00
Sarah Vaupel
2ceced4b64 feat(schedule-opts): enhance course schedule opt messages 2020-11-09 11:30:14 +01:00
Sarah Vaupel
716f31d925 feat(schedule): add model table and course schedule-opt handlers 2020-11-09 11:03:01 +01:00
Sarah Vaupel
ceb4df3c63 chore(schedule): add course schedule-opt routes 2020-11-09 11:02:10 +01:00
Sarah Vaupel
c41e3b6bb3 fix(schedule): display opted-in exam occs without registration 2020-11-07 20:40:11 +01:00
Sarah Vaupel
2bbe67bf90 fix(exams): account for registration in schedule-opt toggle 2020-11-07 20:22:33 +01:00
Sarah Vaupel
a6308544c8 feat(exams): implement schedule-opt actions 2020-11-07 20:09:00 +01:00
Sarah Vaupel
551f64a842 chore(schedule): enhance messages 2020-11-07 19:37:13 +01:00
Sarah Vaupel
39a0eedf5a feat(exams): implement schedule-opt handlers 2020-11-07 19:36:41 +01:00
Sarah Vaupel
c6cd121ad4 feat(schedule-week): revert to exam entries linking to EShowR 2020-11-07 16:21:16 +01:00
Sarah Vaupel
cb3f74a2a9 feat(exams): add basic schedule-opt routes and handler struc 2020-11-07 15:52:58 +01:00
Sarah Vaupel
7f48a2d693 feat(schedule-week): exam entries link to CShowR 2020-11-07 15:28:19 +01:00
Sarah Vaupel
43e5a67164 feat(tutorials): implement tutorial schedule opt handlers 2020-11-07 15:13:03 +01:00
Sarah Vaupel
fd276879ad feat(tutorials): first stub of schedule-opt buttons 2020-11-06 15:55:06 +01:00
Sarah Vaupel
0ecc3c689f feat(course-events): implement opt deletion 2020-11-05 22:54:35 +01:00
Sarah Vaupel
5f9aad8aa9 feat(course-events): implement opt toggle on CShowR 2020-11-05 21:25:50 +01:00
Sarah Vaupel
868a4afcc6 feat(course-events): implement opt handler 2020-11-05 18:09:17 +01:00
Sarah Vaupel
d0fe60b951 Merge branch 'master' into stundenplan 2020-11-05 16:17:43 +01:00
Sarah Vaupel
c984947598 feat(course-events): add basic (un)subscribe routes and handler 2020-11-05 16:17:24 +01:00
Sarah Vaupel
d8a61ed307 feat(schedule): implement explanatory text 2020-11-05 12:32:18 +01:00
Sarah Vaupel
2c62a988df Merge branch 'master' into stundenplan 2020-11-05 00:05:09 +01:00
Sarah Vaupel
c7e6c3c086 feat(schedule): add occurrence-wise opt-in/out
TODO: add interface triggers to insert, update and delete opt-in/out
2020-11-04 00:05:44 +01:00
Sarah Vaupel
51984cde87 Merge branch 'master' into stundenplan 2020-11-03 23:11:25 +01:00
Sarah Vaupel
61545cade0 feat(schedule-week): account for weekdays to hide 2020-11-03 23:10:30 +01:00
Sarah Vaupel
6f4891bb90 feat(schedule-week): hide weekdays according to user settings 2020-10-30 14:42:51 +01:00
Sarah Vaupel
e2b2b8e7e1 feat: displayed weekdays in settings 2020-10-30 14:34:18 +01:00
Sarah Vaupel
766397d114 feat(schedule): account for display default for exam occurrences 2020-10-29 15:23:33 +01:00
Sarah Vaupel
5e0737d1b1 feat(schedule): account for display default for tutorials 2020-10-29 14:28:36 +01:00
Sarah Vaupel
e21536f85d fix: add missing schedule fields to db fill, rename field 2020-10-29 14:27:55 +01:00
Sarah Vaupel
8b49bf866e feat(schedule): use occ display default for course events 2020-10-29 12:49:30 +01:00
Sarah Vaupel
ef8c572860 chore: rename schedule display default field 2020-10-29 12:48:45 +01:00
Sarah Vaupel
4a1002c2ce feat(profile): add new courses display default to user 2020-10-29 11:42:43 +01:00
Sarah Vaupel
52d027259f feat(profile): implement profile form validation wrt timeslots 2020-10-28 21:25:49 +01:00
Sarah Vaupel
3b90b9caa9 fix(profile): add missing fields to user update 2020-10-28 20:01:47 +01:00
Sarah Vaupel
4f13bd422c feat(schedule-week): use user timeslot settings
TODO/FIXME: timeslot handling needs major cleanup
2020-10-28 19:56:06 +01:00
Sarah Vaupel
6aaa5cc477 feat: add timeslot related settings to user and profile form 2020-10-28 19:41:22 +01:00
Sarah Vaupel
dc4bbbd97b refactor(schedule-week): split TimeSlot module into types and definitions 2020-10-28 18:42:50 +01:00
Sarah Vaupel
145564cf77 feat(schedule-week): use userWeekStart in weekly schedule 2020-10-28 17:47:26 +01:00
Sarah Vaupel
0c9671b3d9 feat: add week start to user settings 2020-10-28 17:39:13 +01:00
Sarah Vaupel
4151f62fa5 feat(schedule): enhance messages, restructure profile form 2020-10-28 16:11:32 +01:00
Sarah Vaupel
f5713fdb65 feat(schedule): lookup and use user default schedule view 2020-10-27 12:26:26 +01:00
Sarah Vaupel
b757acb522 feat(profile): add default schedule view to profile form 2020-10-27 12:22:10 +01:00
Sarah Vaupel
304a60560d Merge branch 'master' into stundenplan 2020-10-27 10:25:51 +01:00
Sarah Vaupel
913320a2e9 chore: add default schedule view to user settings 2020-10-26 11:09:19 +01:00
Sarah Vaupel
0d8a613ad6 fix(schedule): import remaining schedule types, fix imports 2020-10-25 14:09:31 +01:00
Sarah Vaupel
6c0a0a2f53 fix(schedule): move schedule utils file 2020-10-25 13:58:03 +01:00
Sarah Vaupel
df3262b8a0 refactor(schedule): move schedule utils 2020-10-24 20:34:21 +02:00
Sarah Vaupel
57c1cc768c fix(schedule): fix options PathPiece instances 2020-10-24 20:02:54 +02:00
Sarah Vaupel
db7238da5e refactor(schedule): use ScheduleOffset in ScheduleOptionsAction 2020-10-24 19:40:17 +02:00
Sarah Vaupel
ae0e3f797f fix(schedule): reintroduce form failure handling 2020-10-24 19:29:32 +02:00
Sarah Vaupel
fe4507cdad chore(schedule): add options instances 2020-10-24 18:59:39 +02:00
Sarah Vaupel
3489ef7926 feat(schedule): move option control below schedule 2020-10-24 17:42:20 +02:00
Sarah Vaupel
fc238ab474 style(schedule): implement option button style 2020-10-24 17:32:47 +02:00
Sarah Vaupel
04341d2e49 fix(schedule): button without value parse 2020-10-24 15:38:32 +02:00
Sarah Vaupel
cb61482b83 refactor(schedule): remove deprecated Button instances 2020-10-23 15:01:23 +02:00
Sarah Vaupel
2c021d0ae2 refactor(schedule): minor handler code cleanup 2020-10-23 14:53:54 +02:00
Sarah Vaupel
ae753e5a4f feat(schedule): implement actions field for options 2020-10-23 14:43:33 +02:00
Sarah Vaupel
be442c6058 refactor(schedule): cleanup debug stuff 2020-10-23 11:22:03 +02:00
Sarah Vaupel
c996049b3f fix(schedule): treat ScheduleOffsetDays 0 similar to ScheduleOffsetNone wrt disabled 2020-10-23 11:17:27 +02:00
Sarah Vaupel
d03a7149a4 feat(schedule): implement correct day offset behaviour 2020-10-23 11:11:04 +02:00
Sarah Vaupel
be700882e1 fix(schedule): fix PathPiece instances, option instead of view/offset buttons 2020-10-22 18:32:53 +02:00
Sarah Vaupel
acb663c480 Merge branch 'master' into stundenplan 2020-10-22 13:19:03 +02:00
Sarah Vaupel
e8adafd123 chore(schedule): add control styling stub 2020-10-10 15:47:02 +02:00
Sarah Vaupel
c866acf600 feat(news): add link to ScheduleR 2020-10-10 15:19:56 +02:00
Sarah Vaupel
eeb365ab5c refactor: avoid day shadowing 2020-10-10 15:14:22 +02:00
Sarah Vaupel
a9b791c554 Merge branch 'master' into stundenplan 2020-10-10 14:58:03 +02:00
Sarah Vaupel
7241afd9d2 refactor(schedule): join view and offset params 2020-09-18 14:49:27 +02:00
Sarah Vaupel
d7366652bf fix(schedule): ignore FormFailures for now 2020-09-18 12:11:44 +02:00
Sarah Vaupel
4316606743 refactor(schedule): enhance options type and instances 2020-09-18 11:45:58 +02:00
Sarah Vaupel
1de2c7f9d3 refactor(schedule-offset): add current offset to offset type 2020-09-18 09:47:00 +02:00
Sarah Vaupel
3cf0188d2a refactor(schedule): catMaybes 2020-09-17 18:35:00 +02:00
Sarah Vaupel
5da9a1499c fix(schedule): switch to mopt 2020-09-17 18:25:15 +02:00
Sarah Vaupel
11c5aa0f10 fix(schedule): add csrf 2020-09-17 18:18:33 +02:00
Sarah Vaupel
44de231f01 fix(schedule): remove unnecessary spans 2020-09-17 17:55:00 +02:00
Sarah Vaupel
9c36c2fb85 Merge branch 'master' into stundenplan 2020-09-17 17:04:49 +02:00
Sarah Vaupel
2d921ba20b fix(schedule): return FormFailure 2020-09-03 20:49:35 +02:00
Sarah Vaupel
9fb4aa1429 refactor(schedule): move utils 2020-09-01 11:15:23 +02:00
Sarah Vaupel
2283a881be refactor(schedule): minor cleanup, add debug/info logs 2020-08-30 15:09:47 +02:00
Sarah Vaupel
e43009ba0c feat(schedule): semi-working offset buttons 2020-08-29 15:50:06 +02:00
Sarah Vaupel
d3afd526ed refactor(schedule): split up types, implement btn class 2020-08-29 14:31:53 +02:00
Sarah Vaupel
02767b4c5b feat(schedule): first (not-yet-working) stub for ScheduleR 2020-08-29 01:38:24 +02:00
Sarah Vaupel
6ed4eab44f fix(tooltips): no waalkes flag style tooltip content 2020-08-27 23:52:10 +02:00
Sarah Vaupel
dddb2746e5 chore(icons): add more icons 2020-08-27 23:32:43 +02:00
Sarah Vaupel
66352522da style(tooltips): fix tooltip styling in button elems 2020-08-27 23:32:15 +02:00
Sarah Vaupel
390a53b982 Merge branch 'master' into stundenplan 2020-08-26 10:31:16 +02:00
Sarah Vaupel
a651e3d62d refactor(schedule-week): add small timslot convenience function 2020-08-25 14:00:41 +02:00
Sarah Vaupel
2428e5ec72 fix(schedule-week): fix allTimeSlots 2020-08-25 12:47:40 +02:00
Sarah Vaupel
3be331f043 feat(schedule-week): display slots outside default range (WIP) 2020-08-25 12:00:37 +02:00
Sarah Vaupel
67302a5dd1 refactor(schedule-week): use dayNowOffset 2020-08-24 23:21:56 +02:00
Sarah Vaupel
798a0811b7 refactor(schedule-week): refactor types and reorganize 2020-08-24 23:18:35 +02:00
Sarah Vaupel
78de1d56ae refactor(schedule-week): minor TimeSlot refactor 2020-08-24 22:43:29 +02:00
Sarah Vaupel
a025e57817 refactor(schedule-week): minor TimeSlot documentation refactor 2020-08-24 19:42:15 +02:00
Sarah Vaupel
2baf76f138 refactor(schedule-week): minor TimeSlot type refactor 2020-08-24 19:39:27 +02:00
Sarah Vaupel
d8227dcf8d refactor(schedule): move event queries 2020-08-24 16:04:37 +02:00
Sarah Vaupel
ed40b89bfe refactor(schedule-week): refactor and fix sql queries 2020-08-24 11:47:16 +02:00
Sarah Vaupel
ed5101c26c refactor(schedule): major ScheduleEntry type refactor 2020-08-24 10:57:16 +02:00
Sarah Vaupel
280a19865c feat(schedule-week): join multiple parallel exam occurrences (WIP) 2020-08-23 21:05:08 +02:00
Sarah Vaupel
7856aba24d refactor(schedule-week): minor TimeSlot refactor 2020-08-23 18:12:03 +02:00
Sarah Vaupel
113f21fc29 fix(schedule-week): correctly display exam occurrences > 1d 2020-08-23 17:46:33 +02:00
Sarah Vaupel
5bd0e7d050 refactor(schedule-week): exclude exams without occurrences by join 2020-08-23 17:25:00 +02:00
Sarah Vaupel
d19be72f58 chore(schedule-week): implement dayOffset 2020-08-23 16:59:47 +02:00
Sarah Vaupel
7c4dc0d6d6 fix(schedule-week): exclude day information from timeslot 2020-08-21 17:38:37 +02:00
Sarah Vaupel
c0fb5adec0 refactor(schedule-week): first step to replace TimeSlot 2020-08-21 17:24:50 +02:00
Sarah Vaupel
428b8cf739 refactor(schedule-week): remove deprecated week messages 2020-08-21 17:14:56 +02:00
Sarah Vaupel
db49943baf refactor(schedule-week): move TimeSlot to separate module 2020-08-21 14:08:09 +02:00
Sarah Vaupel
d82c6b073f refactor(schedule-week): implement utcTime helper function 2020-08-21 13:46:25 +02:00
Sarah Vaupel
2a82ac62e4 fix(schedule-week): fix UTCTime handling of exam occurrences 2020-08-21 13:39:12 +02:00
Sarah Vaupel
0aae46a0b9 fix(schedule-week): fix exam occurrence in slot check 2020-08-21 13:11:15 +02:00
Sarah Vaupel
75bf13ae16 refactor(schedule-week): better types 2020-08-21 13:05:29 +02:00
Sarah Vaupel
9b869b0bb5 fix(schedule-week): use UTCTime for exam occurrences 2020-08-21 12:53:29 +02:00
Sarah Vaupel
693b36e789 refactor(schedule-week): refactor module 2020-08-20 23:14:23 +02:00
Sarah Vaupel
3416e63f6f feat(schedule): include exam occurrences (WIP) 2020-08-20 22:27:59 +02:00
Sarah Vaupel
3254d34dc4 feat(schedule): better handling of hrefs 2020-08-20 18:53:56 +02:00
Sarah Vaupel
6245079465 feat(schedule): display tutorial name 2020-08-20 18:15:54 +02:00
Sarah Vaupel
4007122265 feat(schedule): include tutorials 2020-08-20 18:10:59 +02:00
Sarah Vaupel
2d38172363 refactor(schedule): prepare to include tutorials 2020-08-20 17:59:58 +02:00
Sarah Vaupel
6ac1dc57d0 feat(schedule): enhance display of days 2020-08-20 17:50:47 +02:00
Sarah Vaupel
38fc5fa986 feat(schedule): omit regular occurrences of inactive terms 2020-08-20 17:37:53 +02:00
Sarah Vaupel
2ea234259b refactor(schedule): better template separation 2020-08-20 17:25:04 +02:00
Sarah Vaupel
9b78a5be12 feat(schedule): stubby display of course events 2020-08-20 17:08:30 +02:00
Sarah Vaupel
6b585f8dae chore(schedule): add first WIP stub 2020-08-19 23:10:23 +02:00
65 changed files with 1752 additions and 209 deletions

View File

@ -282,9 +282,16 @@ user-defaults:
date-time-format: "%d.%m.%Y %R"
date-format: "%d.%m.%y"
time-format: "%R"
week-start: Monday
download-files: false
warning-days: 1209600
show-sex: false
schedule-view: week
schedule-week-days: [Monday,Tuesday,Wednesday,Thursday,Friday]
schedule-week-time-from: 28800 # 08:00
schedule-week-time-to: 72000 # 20:00
schedule-week-timeslot-length: 7200 # 2h
schedule-occurrence-display-default: true
exam-office-get-synced: true
exam-office-get-labels: true
prefers-postal: true

View File

@ -263,6 +263,13 @@ button:not(.btn-link),
&.btn-danger
background-color: var(--color-error-dark)
.fa,.fas
color: white
.tooltip__handle
cursor: pointer
.tooltip__content
color: var(--color-font)
.buttongroup
display: grid
grid: min-content / auto-flow max-content
@ -280,6 +287,8 @@ button[disabled]:not(.btn-link),
opacity: 0.3
background-color: var(--color-grey)
cursor: default
.tooltip__handle
cursor: default
input[type="submit"]:not([disabled]):not(.btn-link):hover,
input[type="button"]:not([disabled]):not(.btn-link):hover,
@ -1694,6 +1703,63 @@ video
object-fit: contain
flex-grow: 1
table.schedule
.schedule--entry
background-color: var(--color-dark)
color: white
font-weight: 600
padding: 10px
&__ends
border-radius: 0 0 15px 15px
background: linear-gradient(0turn, var(--color-dark), var(--color-dark) calc(100% - 7px), rgba(0,0,0,0))
&__begins
border-radius: 15px 15px 0 0
background: linear-gradient(0.5turn, var(--color-dark), var(--color-dark) calc(100% - 7px), rgba(0,0,0,0))
&__contained
border-radius: 15px
&__intersects
background: linear-gradient(0turn, rgba(0,0,0,0), var(--color-dark) 7px, var(--color-dark) calc(100% - 7px), rgba(0,0,0,0))
&__continuation
font-style: italic
a.schedule--entry-link
text-decoration: none
a.schedule--entry-link + a.schedule--entry-link > .schedule--entry
margin-top: 5px
a
color: white
.table__th.schedule-current
background-color: var(--color-primary)
.schedule--entry.schedule-current
@keyframes schedule-current--blink
50%
opacity: .85
animation: schedule-current--blink 2s linear infinite
form.schedule-options
--schedule-option-radius: 20px 50%
display: flex
justify-content: center
button[name=schedule-options]
height: 45px
white-space: nowrap
& > div:first-of-type
button[name=schedule-options]
border-top-left-radius: var(--schedule-option-radius)
border-bottom-left-radius: var(--schedule-option-radius)
& > div:last-of-type
button[name=schedule-options]
border-top-right-radius: var(--schedule-option-radius)
border-bottom-right-radius: var(--schedule-option-radius)
.hr
height: 1px
width: 90%
@ -1760,4 +1826,4 @@ video
color: var(--color-lightwhite)
&.nonactive
background-color: var(--color-nonactive)
color: var(--color-nonactive-dark)
color: var(--color-nonactive-dark)

View File

@ -2,8 +2,6 @@
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#messages or constructors that are used all over the code
Logo !ident-ok: FRADrive
EmailInvitationWarning: Diese Adresse konnte keinem FRADrive-Benutzer/-Benutzerin zugeordnet werden. Es wird eine Einladung per E-Mail versandt.
BoolIrrelevant !ident-ok: —
@ -31,4 +29,4 @@ PaginationPage: Angzeigte Seite
PaginationError: Paginierung Parameter dürfen nicht negativ sein
NullDeletes: Zum Löschen NULL eingeben.
SortPriority: Sortierungspriorität
SortPriority: Sortierungsprioritätz

View File

@ -2,8 +2,6 @@
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#messages or constructors that are used all over the Code
Logo: FRADrive
EmailInvitationWarning: This address could not be matched to any FRADrive user. An invitation will be sent via email.
BoolIrrelevant: —

View File

@ -9,7 +9,7 @@ DegreeCourse json -- for which degree programmes this course is appropriate fo
UniqueDegreeCourse course degree terms
deriving Generic
Course -- Information about a single course; contained info is always visible to all users
name (CI Text)
name CourseName
description StoredMarkup Maybe -- user-defined large Html, ought to contain module description
linkExternal URI Maybe -- arbitrary user-defined url for external course page
shorthand (CI Text) -- practical shorthand of course name, used for identification
@ -27,8 +27,17 @@ Course -- Information about a single course; contained info is always visible
TermSchoolCourseShort term school shorthand -- shorthand must be unique within school and semester
TermSchoolCourseName term school name -- name must be unique within school and semester
deriving Generic
CourseScheduleOpt -- opt-in/-out for displaying occurrence related to this course (may be overriden by specific occurrence opts)
course CourseId
user UserId
opt Bool
UniqueCourseScheduleOpt course user
deriving Generic
CourseEvent
type (CI Text)
type CourseEventType
course CourseId
course CourseId OnDeleteCascade OnUpdateCascade
room RoomReference Maybe
roomHidden Bool default=false
@ -36,6 +45,12 @@ CourseEvent
note StoredMarkup Maybe
lastChanged UTCTime default=now()
deriving Generic
CourseEventScheduleOpt -- opt-in/-out for course event display in a user's schedule (TODO: currently for all occurrences of a course event; separate opt-ins/-outs per occurrence in CourseEventTime instead?)
courseEvent CourseEventId
user UserId
opt Bool -- whether the course event should be displayed; False <=> opt-out, True <=> opt-in
UniqueCourseEventScheduleOpt courseEvent user
deriving Generic
CourseAppInstructionFile
course CourseId OnDeleteCascade OnUpdateCascade

View File

@ -47,6 +47,12 @@ ExamOccurrence
description StoredMarkup Maybe
UniqueExamOccurrence exam name
deriving Generic
ExamOccurrenceScheduleOpt
examOccurrence ExamOccurrenceId
user UserId
opt Bool
UniqueExamOccurrenceScheduleOpt examOccurrence user
deriving Generic
ExamRegistration
exam ExamId
user UserId

View File

@ -1,11 +1,11 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
Tutorial json
name TutorialName
course CourseId OnDeleteCascade OnUpdateCascade
type (CI Text) -- "Tutorium", "Zentralübung", ...
type TutorialType -- "Tutorium", "Zentralübung", ...
capacity Int Maybe -- limit for enrolment in this tutorial
room RoomReference Maybe
roomHidden Bool default=false
@ -29,4 +29,10 @@ TutorialParticipant
user UserId
UniqueTutorialParticipant tutorial user
deriving Eq Ord Show
deriving Generic
deriving Generic
TutorialScheduleOpt
tutorial TutorialId
user UserId
opt Bool
UniqueTutorialScheduleOpt tutorial user
deriving Generic

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -32,6 +32,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create
dateTimeFormat DateTimeFormat "default='%d %b %y %R'" -- preferred Date+Time display format for user; user-defined
dateFormat DateTimeFormat "default='%d %b %Y'" -- preferred Date-only display format for user; user-defined
timeFormat DateTimeFormat "default='%R'" -- preferred Time-only display format for user; user-defined
weekStart DayOfWeek default='monday' -- preferred first day of week for user; user-defined
downloadFiles Bool default=false -- Should files be opened in browser or downloaded? (users often oblivious that their browser has a setting for this)
languages Languages Maybe -- Preferred language; user-defined
notificationSettings NotificationSettings "default='{}'::jsonb" -- Bit-array for which events email notifications are requested by user; user-defined; missing fields in json object will be parsed to default trigger
@ -40,6 +41,12 @@ User json -- Each Uni2work user has a corresponding row in this table; create
sex Sex Maybe -- currently ignored
birthday Day Maybe -- for better identification
showSex Bool default=false
scheduleView ScheduleView default='week'
scheduleWeekDays ScheduleWeekDays default='["monday","tuesday","wednesday","thursday","friday"]'::jsonb -- which weekdays to display by default; if there is an occurrence to display for a weekday that is not mentioned here, the weekday will be displayed regardless
scheduleWeekTimeFrom NominalDiffTime default=28800 -- start of the first time slot to display in weekly schedule by default (i.e. regardless of the existence of occurrences in this slot)
scheduleWeekTimeTo NominalDiffTime default=72000 -- end of the last time slot to display in weekly schedule by default (i.e. regardless of the existence of occurrences in this slot)
scheduleWeekTimeslotLength NominalDiffTime default=7200 -- length of one timeslot
scheduleOccurrenceDisplayDefault Bool default=True -- whether occurrences from new courses should be displayed in the schedule by default
telephone Text Maybe
mobile Text Maybe
companyPersonalNumber Text Maybe -- Company will become a new table, but if company=fraport, some information is received via LDAP

16
routes
View File

@ -170,7 +170,10 @@
/course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer:
/ CShowR GET !tutor !corrector !exam-corrector !course-registered !course-time !evaluation !exam-office
/favourite CFavouriteR GET POST !free
/schedule-opt/set/#Bool CScheduleOptSetR GET POST !free
/schedule-opt/del CScheduleOptDelR GET POST !free
/register CRegisterR GET POST !timeANDcapacityAND¬course-registeredANDcourse-time !timeAND¬exam-resultANDcourse-registered !lecturer
/register-template CRegisterTemplateR GET !course-time
/edit CEditR GET POST
/lecturer-invite CLecInviteR GET POST
/delete CDeleteR GET POST !lecturerANDempty
@ -230,6 +233,8 @@
/register TRegisterR POST !timeANDcapacityANDcourse-registeredANDregister-group !timeANDtutorial-registered
/communication TCommR GET POST !tutor
/tutor-invite TInviteR GET POST !tutorANDtutor-control
/schedule-opt/set/#Bool TScheduleOptSetR GET POST !free
/schedule-opt/del TScheduleOptDelR GET POST !free
/exams CExamListR GET !tutor !corrector !exam-corrector !course-registered !course-time !exam-office
/exams/new CExamNewR GET POST
/exams/#ExamName ExamR:
@ -241,6 +246,8 @@
/users/invite EInviteR GET POST
/register ERegisterR POST !timeANDcourse-registeredAND¬exam-registered !timeANDexam-registeredAND¬exam-result
/register/#ExamOccurrenceName ERegisterOccR POST !exam-occurrence-registrationANDtimeANDcapacityANDcourse-registeredAND¬exam-occurrence-registered !exam-occurrence-registrationANDtimeANDexam-occurrence-registeredAND¬exam-result
/schedule-opt/#ExamOccurrenceName/set/#Bool EScheduleOptSetR GET POST !free -- TODO: restrict to !timeANDcourse-registered !lecturer !tutor !corrector
/schedule-opt/#ExamOccurrenceName/del EScheduleOptDelR GET POST !free -- TODO: see above
/grades EGradesR GET POST !exam-office
/assign-occurrences EAutoOccurrenceR POST
/correct ECorrectR GET POST !exam-correctorANDtime
@ -253,9 +260,11 @@
!/download/*FilePath CNFileR GET !timeANDparticipant
!/events/add CEventsNewR GET POST
/events/#CryptoUUIDCourseEvent CourseEventR:
/edit CEvEditR GET POST
/delete CEvDeleteR GET POST
/personalised-sheet-files CPersonalFilesR GET
/schedule-opt/set/#Bool CEvScheduleOptSetR GET POST !free
/schedule-opt/delete CEvScheduleOptDelR GET POST !free
/edit CEvEditR GET POST
/delete CEvDeleteR GET POST
/personalised-sheet-files CPersonalFilesR GET
/subs CorrectionsR GET POST !corrector !lecturer
@ -270,6 +279,7 @@
/msg/#{CryptoUUIDSystemMessage}/hide MessageHideR POST !timeANDauthentication
/schedule ScheduleR GET POST !free
/upload UploadR PUT !free

View File

@ -144,6 +144,7 @@ import Handler.Tutorial
import Handler.Material
import Handler.CryptoIDDispatch
import Handler.SystemMessage
import Handler.Schedule
import Handler.Health
import Handler.Health.Interface
import Handler.Exam

View File

@ -1,9 +1,7 @@
-- SPDX-FileCopyrightText: 2023-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2023 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
{-# LANGUAGE TypeApplications #-}
module Audit
( module Audit.Types
, AuditException(..)
@ -19,8 +17,6 @@ import Import.NoModel
import Settings
import Model
import Database.Persist.Sql
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
import qualified Database.Esqueleto.Utils as E
import Audit.Types
import qualified Data.Text as Text
@ -133,7 +129,7 @@ logInterface :: ( AuthId (HandlerSite m) ~ Key User
-> Text -- ^ Any additional information
-> ReaderT (YesodPersistBackend (HandlerSite m)) m ()
-- ^ Log a transaction using information available from `HandlerT`, also calls `audit`
logInterface interfaceLogInterface interfaceLogSubtype interfaceLogSuccess interfaceLogRows interfaceLogInfo = do
logInterface interfaceLogInterface interfaceLogSubtype interfaceLogSuccess interfaceLogRows interfaceLogInfo = do
interfaceLogWrite <- (methodGet /=) . Wai.requestMethod . reqWaiRequest <$> getRequest
logInterface' interfaceLogInterface interfaceLogSubtype interfaceLogWrite interfaceLogSuccess interfaceLogRows interfaceLogInfo
@ -177,25 +173,20 @@ logInterface' (Text.strip -> interfaceLogInterface) (Text.strip -> interfaceLogS
reportAdminProblem :: ( IsSqlBackend (YesodPersistBackend (HandlerSite m))
, SqlBackendCanWrite (YesodPersistBackend (HandlerSite m))
, MonadHandler m
, MonadHandler m
-- , HasCallStack
)
=> AdminProblem -- ^ Problem to record
=> AdminProblem -- ^ Problem to record
-> ReaderT (YesodPersistBackend (HandlerSite m)) m ()
-- ^ Log a problem that needs interventions by admins, provided this problem has not already been reported and is still unsolved
-- ^ Log a problem that needs interventions by admins
--
-- - `problemLogTime` is now
-- - `problemSolver` is Nothing, we do not record the person who caused it
reportAdminProblem problem = do
reportAdminProblem problem@(toJSON -> problemLogInfo) = do
problemLogTime <- liftIO getCurrentTime
let problemLogSolved = Nothing
problemLogSolver = Nothing
problemLogInfo = toJSON problem
problemLogTime <- liftIO getCurrentTime
isKnown <- E.selectExists $ do
pl <- E.from $ E.table @ProblemLog
E.where_ $ E.isNothing (pl E.^. ProblemLogSolved)
E.&&. E.val problemLogInfo E.==. pl E.^. ProblemLogInfo
unless isKnown $ insert_ ProblemLog{..}
insert_ ProblemLog{..}
$logWarnS "Problem" $ Text.filter (/= '\n') $ tshow problem -- <> " - " <> pack (prettyCallStack callStack)

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -38,7 +38,7 @@ module Database.Esqueleto.Utils
, SqlHashable
, sha256
, isTrue, isFalse
, maybe, maybe2, maybeEq, guardMaybe, unsafeCoalesce
, maybe, maybe2, maybeEq, fromMaybe, guardMaybe, unsafeCoalesce
, bool
, max, min
, greatest, least
@ -61,7 +61,7 @@ module Database.Esqueleto.Utils
) where
import ClassyPrelude.Yesod hiding (isInfixOf, any, all, or, and, isJust, maybe, bool, max, min, abs)
import ClassyPrelude.Yesod hiding (isInfixOf, any, all, or, and, isJust, maybe, fromMaybe, bool, max, min, abs)
import Data.Universe
import qualified Data.Set as Set
import qualified Data.List as List
@ -621,6 +621,13 @@ maybeEq a b = E.case_
]
(E.else_ $ a E.==. b)
-- TODO: replace with guardMaybe in Utils.Schedule
fromMaybe :: (PersistField a)
=> E.SqlExpr (E.Value a)
-> E.SqlExpr (E.Value (Maybe a))
-> E.SqlExpr (E.Value a)
fromMaybe onNothing = maybe onNothing id
guardMaybe :: PersistField a
=> E.SqlExpr (E.Value (Maybe a))
-> E.SqlQuery (E.SqlExpr (E.Value a))

View File

@ -61,6 +61,7 @@ import Data.CaseInsensitive (original, mk)
import qualified Data.Text as Text
import Utils.Form
import Utils.Schedule.Types.ScheduleView
import qualified GHC.Exts (IsList(..))
@ -503,6 +504,13 @@ instance RenderMessage UniWorX CourseParticipantState where
mr :: RenderMessage UniWorX msg => msg -> Text
mr = renderMessage foundation ls
instance RenderMessage UniWorX ScheduleView where
renderMessage foundation ls = \case
ScheduleViewWeek -> mr MsgScheduleViewWeek
where
mr :: RenderMessage UniWorX msg => msg -> Text
mr = renderMessage foundation ls
instance RenderMessage UniWorX ExamCloseMode where
renderMessage foundation ls = \case
ExamCloseSeparate -> mr MsgExamCloseModeSeparate

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -25,6 +25,7 @@ data instance ButtonClass UniWorX
| BCDanger
| BCLink
| BCMassInputAdd | BCMassInputDelete
| BCScheduleView | BCScheduleOffset
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic)
deriving anyclass (Universe, Finite)

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -265,6 +265,8 @@ breadcrumb (CourseR tid ssh csh CLecInviteR) = i18nCrumb MsgBreadcrumbLecturerIn
breadcrumb (CourseR tid ssh csh CDeleteR) = i18nCrumb MsgMenuCourseDelete . Just $ CourseR tid ssh csh CShowR
breadcrumb (CourseR tid ssh csh CHiWisR) = i18nCrumb MsgBreadcrumbHiWis . Just $ CourseR tid ssh csh CShowR
breadcrumb (CourseR tid ssh csh CNotesR) = i18nCrumb MsgBreadcrumbCourseNotes . Just $ CourseR tid ssh csh CShowR
breadcrumb (CourseR tid ssh csh (CScheduleOptSetR _opt)) = i18nCrumb MsgBreadcrumbScheduleOpt . Just $ CourseR tid ssh csh CShowR
breadcrumb (CourseR tid ssh csh CScheduleOptDelR) = i18nCrumb MsgBreadcrumbScheduleOpt . Just $ CourseR tid ssh csh CShowR
breadcrumb (CourseR tid ssh csh CNewsNewR) = i18nCrumb MsgMenuCourseNewsNew . Just $ CourseR tid ssh csh CShowR
breadcrumb (CourseR tid ssh csh (CourseNewsR cID sRoute)) = case sRoute of
@ -276,6 +278,8 @@ breadcrumb (CourseR tid ssh csh (CourseNewsR cID sRoute)) = case sRoute of
breadcrumb (CourseR tid ssh csh CEventsNewR) = i18nCrumb MsgMenuCourseEventNew . Just $ CourseR tid ssh csh CShowR
breadcrumb (CourseR tid ssh csh (CourseEventR _cID sRoute)) = case sRoute of
CEvScheduleOptSetR _ -> i18nCrumb MsgBreadcrumbScheduleOpt . Just $ CourseR tid ssh csh CShowR
CEvScheduleOptDelR -> i18nCrumb MsgBreadcrumbScheduleOpt . Just $ CourseR tid ssh csh CShowR
CEvEditR -> i18nCrumb MsgMenuCourseEventEdit . Just $ CourseR tid ssh csh CShowR
CEvDeleteR -> i18nCrumb MsgBreadcrumbCourseEventDelete . Just $ CourseR tid ssh csh CShowR
@ -296,6 +300,8 @@ breadcrumb (CourseR tid ssh csh (ExamR examn sRoute)) = case sRoute of
ERegisterOccR _occn -> i18nCrumb MsgBreadcrumbExamRegister . Just $ CExamR tid ssh csh examn EShowR
EAutoOccurrenceR -> i18nCrumb MsgBreadcrumbExamAutoOccurrence . Just $ CExamR tid ssh csh examn EUsersR
ECorrectR -> i18nCrumb MsgMenuExamCorrect . Just $ CExamR tid ssh csh examn EShowR
EScheduleOptSetR _eoname _opt -> i18nCrumb MsgBreadcrumbScheduleOpt . Just $ CExamR tid ssh csh examn EShowR
EScheduleOptDelR _eoname -> i18nCrumb MsgBreadcrumbScheduleOpt . Just $ CExamR tid ssh csh examn EShowR
breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of
TUsersR -> useRunDB . maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do
@ -307,6 +313,8 @@ breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of
TCommR -> i18nCrumb MsgMenuTutorialComm . Just $ CTutorialR tid ssh csh tutn TUsersR
TRegisterR -> i18nCrumb MsgBreadcrumbTutorialRegister . Just $ CourseR tid ssh csh CShowR
TInviteR -> i18nCrumb MsgBreadcrumbTutorInvite . Just $ CTutorialR tid ssh csh tutn TUsersR
(TScheduleOptSetR _) -> i18nCrumb MsgBreadcrumbScheduleOpt . Just $ CourseR tid ssh csh CTutorialListR
TScheduleOptDelR -> i18nCrumb MsgBreadcrumbScheduleOpt . Just $ CourseR tid ssh csh CTutorialListR
breadcrumb (CourseR tid ssh csh (SheetR shn sRoute)) = case sRoute of
SShowR -> useRunDB . maybeT (i18nCrumb MsgBreadcrumbSheet . Just $ CourseR tid ssh csh SheetListR) $ do
@ -369,6 +377,8 @@ breadcrumb (MessageR _) = do
breadcrumb MessageListR = i18nCrumb MsgMenuMessageList $ Just AdminR
breadcrumb (MessageHideR cID) = i18nCrumb MsgBreadcrumbMessageHide . Just $ MessageR cID
breadcrumb ScheduleR = i18nCrumb MsgMenuSchedule Nothing
breadcrumb GlossaryR = i18nCrumb MsgMenuGlossary $ Just InfoR
breadcrumb EExamListR = i18nCrumb MsgMenuExternalExamList Nothing

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -303,6 +303,12 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
, userPostLastUpdate = Nothing
, userPinPassword = Nothing -- must be derived via AVS
, userPrefersPostal = userDefaultPrefersPostal
, userScheduleView = userDefaultScheduleView
, userScheduleWeekDays = userDefaultScheduleWeekDays
, userScheduleWeekTimeFrom = userDefaultScheduleWeekTimeFrom
, userScheduleWeekTimeTo = userDefaultScheduleWeekTimeTo
, userScheduleWeekTimeslotLength = userDefaultScheduleWeekTimeslotLength
, userScheduleOccurrenceDisplayDefault = userDefaultScheduleOccurrenceDisplayDefault
, ..
}
userUpdate =

View File

@ -25,6 +25,7 @@ import Handler.ExamOffice.Course as Handler.Course
import Handler.Course.News as Handler.Course
import Handler.Course.Events as Handler.Course
import Handler.Sheet.PersonalisedFiles as Handler.Course (getCPersonalFilesR)
import Handler.Course.Schedule as Handler.Course
getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html

View File

@ -9,3 +9,4 @@ module Handler.Course.Events
import Handler.Course.Events.New as Handler.Course.Events
import Handler.Course.Events.Edit as Handler.Course.Events
import Handler.Course.Events.Delete as Handler.Course.Events
import Handler.Course.Events.Schedule as Handler.Course.Events

View File

@ -0,0 +1,36 @@
module Handler.Course.Events.Schedule
( getCEvScheduleOptSetR , postCEvScheduleOptSetR
, getCEvScheduleOptDelR , postCEvScheduleOptDelR
) where
import Import
getCEvScheduleOptSetR, postCEvScheduleOptSetR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseEvent -> Bool -> Handler Html
getCEvScheduleOptSetR = postCEvScheduleOptSetR
postCEvScheduleOptSetR tid ssh csh ceId opt = do
uid <- requireAuthId
eId <- decrypt ceId
runDB $ void $ upsert (CourseEventScheduleOpt
{ courseEventScheduleOptCourseEvent = eId
, courseEventScheduleOptUser = uid
, courseEventScheduleOptOpt = opt
})
[ CourseEventScheduleOptOpt =. opt
]
addMessageI Success $ bool MsgScheduleOptOutSuccess MsgScheduleOptInSuccess opt
redirect $ CourseR tid ssh csh CShowR
getCEvScheduleOptDelR, postCEvScheduleOptDelR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDCourseEvent -> Handler Html
getCEvScheduleOptDelR = postCEvScheduleOptDelR
postCEvScheduleOptDelR tid ssh csh ceId = do
uid <- requireAuthId
eId <- decrypt ceId
runDB $ deleteBy (UniqueCourseEventScheduleOpt eId uid)
addMessageI Success MsgScheduleOptDeleteSuccess
redirect $ CourseR tid ssh csh CShowR

View File

@ -0,0 +1,41 @@
module Handler.Course.Schedule
( getCScheduleOptSetR, postCScheduleOptSetR
, getCScheduleOptDelR, postCScheduleOptDelR
) where
import Import
getCScheduleOptSetR, postCScheduleOptSetR :: TermId -> SchoolId -> CourseShorthand -> Bool -> Handler Html
getCScheduleOptSetR = postCScheduleOptSetR
postCScheduleOptSetR tid ssh csh opt = do
uid <- requireAuthId
mResult <- runDB $ maybeT (return Nothing) $ do
cid <- MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
scheduleOpt <- lift $ upsert (CourseScheduleOpt
{ courseScheduleOptCourse = cid
, courseScheduleOptUser = uid
, courseScheduleOptOpt = opt
})
[ CourseScheduleOptOpt =. opt
]
return $ Just scheduleOpt
case mResult of
Just (Entity _ CourseScheduleOpt{..}) -> addMessageI Success $ bool MsgCourseScheduleOptOutSuccess MsgCourseScheduleOptInSuccess courseScheduleOptOpt
Nothing -> addMessageI Error MsgCourseScheduleOptError
redirect $ CourseR tid ssh csh CShowR
getCScheduleOptDelR, postCScheduleOptDelR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCScheduleOptDelR = postCScheduleOptDelR
postCScheduleOptDelR tid ssh csh = do
uid <- requireAuthId
runDB $ maybeT (return ()) $ do
cid <- MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
lift . deleteBy $ UniqueCourseScheduleOpt cid uid
addMessageI Success MsgCourseScheduleOptDeleteSuccess
redirect $ CourseR tid ssh csh CShowR

View File

@ -10,6 +10,7 @@ import Import
import Utils.Course
import Utils.Form
import Utils.Schedule
import Handler.Utils
import Handler.Utils.Course
import Handler.Utils.Tutorial
@ -29,14 +30,16 @@ import Handler.Exam.List (mkExamTable)
getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCShowR tid ssh csh = do
mbAid <- maybeAuthId
mbAuth <- maybeAuthPair
now <- liftIO getCurrentTime
(cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,administrators,correctors,tutors,news,events,submissionGroup,_mayReRegister,(mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial),courseQualifications) <- runDB . maybeT notFound $ do
ata <- getSessionActiveAuthTags
(cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,administrators,correctors,tutors,news,events,submissionGroup,_mayReRegister,mCourseScheduleOpt,mayReRegister,(mayViewSheets,mayViewAnySheet),(mayViewMaterials,mayViewAnyMaterial),courseQualifications) <- runDB . maybeT notFound $ do
[(E.Entity cid course, E.Value courseVisible, E.Value schoolName, E.Value participants, fmap entityVal -> registration)]
<- lift . E.select . E.from $
\((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do
E.on $ E.just (course E.^. CourseId) E.==. participant E.?. CourseParticipantCourse
E.&&. E.val mbAid E.==. participant E.?. CourseParticipantUser
E.&&. E.val (fst <$> mbAuth) E.==. participant E.?. CourseParticipantUser
E.&&. participant E.?. CourseParticipantState E.==. E.just (E.val CourseParticipantActive)
E.on $ course E.^. CourseSchool E.==. school E.^. SchoolId
E.where_ $ course E.^. CourseTerm E.==. E.val tid
@ -96,17 +99,30 @@ getCShowR tid ssh csh = do
return (cID, n, visible, files, lastEditText, mayEditNews, mayDelete, archiveUrl)
mCourseScheduleOpt <- case mbAuth of
Just (uid,_) -> lift $ getBy $ UniqueCourseScheduleOpt cid uid
Nothing -> return Nothing
events' <- fmap (sortOn $ courseEventTime . entityVal . view _1) . lift . E.select . E.from $ \courseEvent -> do
E.where_ $ courseEvent E.^. CourseEventCourse E.==. E.val cid
let showRoom = maybe E.false (flip showCourseEventRoom courseEvent . E.val) mbAid
let showRoom = maybe E.false (flip showCourseEventRoom courseEvent . E.val . view _1) mbAuth
E.||. E.not_ (courseEvent E.^. CourseEventRoomHidden)
return (courseEvent, showRoom)
events <- mapM (\(Entity evId ev, E.Value showRoom) -> (, ev, showRoom) <$> encrypt evId) events'
events <- forM events' $ \(Entity evId ev, E.Value showRoom) -> do
evId' <- encrypt evId
shouldBeDisplayedInSchedule <- lift $ E.selectExists . E.from $ \(c `E.InnerJoin` cEv) -> do
E.on $ c E.^. CourseId E.==. cEv E.^. CourseEventCourse
E.where_ $ cEv E.^. CourseEventId E.==. E.val evId
E.&&. courseEventShouldBeDisplayedInSchedule (view _1 <$> mbAuth) ata c cEv
mCourseEventScheduleOpt <- case mbAuth of
Just (aid,_) -> lift $ getBy $ UniqueCourseEventScheduleOpt evId aid
Nothing -> return Nothing
return (evId', ev, showRoom, shouldBeDisplayedInSchedule, mCourseEventScheduleOpt)
hasSubmissionGroups <- lift . E.selectExists . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup) -> do
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid
submissionGroup' <- lift . for mbAid $ \uid ->
submissionGroup' <- lift . for mbAuth $ \(uid,_) ->
fmap (listToMaybe . fmap E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup) -> do
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid
@ -128,14 +144,14 @@ getCShowR tid ssh csh = do
return $ material E.^. MaterialName
mayViewAnyMaterial <- lift . anyM materials $ \(E.Value mnm) -> hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR
courseQualifications <- lift $ getCourseQualifications cid
courseQualifications <- lift $ getCourseQualifications cID
return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,administrators,correctors,tutors,news,events,submissionGroup,mayReRegister, (mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial), courseQualifications)
return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,administrators,correctors,tutors,news,events,submissionGroup,mayReRegister,mCourseScheduleOpt,mayReRegister,(mayViewSheets,mayViewAnySheet),(mayViewMaterials,mayViewAnyMaterial),courseQualifications)
mDereg <- traverse (formatTime SelFormatDateTime) $ courseDeregisterUntil course
regForm <- if
| is _Just mbAid -> do
| is _Just mbAuth -> do
(courseRegisterForm', regButton) <- courseRegisterForm (Entity cid course)
(regWidget, regEnctype) <- generateFormPost $ renderAForm FormStandard courseRegisterForm'
return $ wrapForm' regButton regWidget def
@ -159,7 +175,7 @@ getCShowR tid ssh csh = do
dbtSQLQuery tutorial = do
E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
let showRoom = maybe E.false (flip showTutorialRoom tutorial . E.val) mbAid
let showRoom = maybe E.false (flip showTutorialRoom tutorial . E.val . view _1) mbAuth
E.||. E.not_ (tutorial E.^. TutorialRoomHidden)
return (tutorial, showRoom)
dbtRowKey = (E.^. TutorialId)
@ -197,22 +213,49 @@ getCShowR tid ssh csh = do
in return $ E.val tutorialCapacity' E.-. numParticipants
return . toWidget $ tshow freeCapacity
, guardMonoid (not mayMassRegister || isJust registration) $
sortable Nothing (mempty & cellAttrs <>~ pure ("uw-hide-columns--hider-label", mr MsgTableActionsHead)) $ \(view resultTutorial -> Entity tutId Tutorial{..}) -> sqlCell $ do
mayRegister <- (== Authorized) <$> evalAccessDB (CTutorialR tid ssh csh tutorialName TRegisterR) True
isRegistered <- case mbAid of
Nothing -> return False
Just uid -> existsBy $ UniqueTutorialParticipant tutId uid
if
| mayRegister -> do
(tutRegisterForm, tutRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered
return $ wrapForm tutRegisterForm def
{ formAction = Just . SomeRoute $ CTutorialR tid ssh csh tutorialName TRegisterR
, formEncoding = tutRegisterEnctype
, formSubmit = FormNoSubmit
}
| isRegistered -> return [whamlet|_{MsgTutorialRegistered}|]
| otherwise -> return mempty
, guardMonoid mayMassRegister $
sortable Nothing (mempty & cellAttrs <>~ pure ("uw-hide-columns--hider-label", mr MsgActionsHead)) $ \(view resultTutorial -> Entity tutId Tutorial{..}) -> sqlCell $ do
mayRegister <- (== Authorized) <$> evalAccessDB (CTutorialR tid ssh csh tutorialName TRegisterR) True
isRegistered <- case mbAuth of
Nothing -> return False
Just (uid,_) -> existsBy $ UniqueTutorialParticipant tutId uid
tutRegister <- if
| mayRegister -> do
(tutRegisterForm, tutRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnRegister] [BtnDeregister] isRegistered
return $ wrapForm tutRegisterForm def
{ formAction = Just . SomeRoute $ CTutorialR tid ssh csh tutorialName TRegisterR
, formEncoding = tutRegisterEnctype
, formSubmit = FormNoSubmit
}
| isRegistered -> return [whamlet|_{MsgTutorialRegistered}|]
| otherwise -> return mempty
tutScheduleOptSet <- if
| Just (uid,_) <- mbAuth -> do
shouldBeDisplayedInSchedule <- E.selectExists . E.from $ \(c `E.InnerJoin` tut) -> do
E.on $ c E.^. CourseId E.==. tut E.^. TutorialCourse
E.where_ $ tut E.^. TutorialId E.==. E.val tutId
E.&&. tutorialShouldBeDisplayedInSchedule (Just uid) ata c tut
(tutScheduleForm, tutScheduleEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnScheduleOptIn] [BtnScheduleOptOut] shouldBeDisplayedInSchedule
return $ wrapForm tutScheduleForm def
{ formAction = Just . SomeRoute $ CTutorialR tid ssh csh tutorialName (TScheduleOptSetR $ not shouldBeDisplayedInSchedule)
, formEncoding = tutScheduleEnctype
, formSubmit = FormNoSubmit
}
| otherwise -> return mempty
tutScheduleOptDel <- if
| Just (uid,_) <- mbAuth -> do
mScheduleOpt <- getBy $ UniqueTutorialScheduleOpt tutId uid
if is _Just mScheduleOpt
then do
(tutScheduleOptDelForm, tutScheduleOptDelEnctype) <- liftHandler . generateFormPost . buttonForm' $ [BtnScheduleOptDel]
return $ wrapForm tutScheduleOptDelForm def
{ formAction = Just . SomeRoute $ CTutorialR tid ssh csh tutorialName TScheduleOptDelR
, formEncoding = tutScheduleOptDelEnctype
, formSubmit = FormNoSubmit
}
else return mempty
| otherwise -> return mempty
return $ tutRegister <> tutScheduleOptSet <> tutScheduleOptDel
, guardMonoid mayMassRegister $ -- TODO: schedule opts?
sortable Nothing (mempty & cellAttrs <>~ pure ("uw-hide-columns--hider-label", mr MsgTableActionsHead)) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) ->
cell $ linkButton mempty (msg2widget MsgMassRegister) [BCIsButton, BCPrimary] (SomeRoute $ CTutorialR tid ssh csh tutorialName TAddUserR)
]
@ -253,12 +296,19 @@ getCShowR tid ssh csh = do
, length fs <= 3
, all (views (_1 . _2) $ notElem pathSeparator) fs
]
hiddenEventNotes = all (\(_,CourseEvent{..},_) -> is _Nothing courseEventNote) events
hiddenEventNotes = all (\(_,CourseEvent{..},_,_,_) -> is _Nothing courseEventNote) events
Course{courseVisibleFrom,courseVisibleTo} = course
mayCreateNews <- hasWriteAccessTo $ CourseR tid ssh csh CNewsNewR
mayCreateEvents <- hasWriteAccessTo $ CourseR tid ssh csh CEventsNewR
mayEdit <- hasWriteAccessTo $ CourseR tid ssh csh CEditR
let courseScheduleOptToggleValue User{userScheduleOccurrenceDisplayDefault} = maybe
( userScheduleOccurrenceDisplayDefault
&& ( is _Just registration )
)
(courseScheduleOptOpt . entityVal)
mCourseScheduleOpt
let heading = [whamlet|
$newline never
^{courseName course}

View File

@ -13,6 +13,7 @@ import Handler.Exam.RegistrationInvite as Handler.Exam
import Handler.Exam.New as Handler.Exam
import Handler.Exam.Edit as Handler.Exam
import Handler.Exam.Show as Handler.Exam
import Handler.Exam.Schedule as Handler.Exam
import Handler.Exam.Users as Handler.Exam
import Handler.Exam.AddUser as Handler.Exam
import Handler.Exam.AutoOccurrence as Handler.Exam

View File

@ -0,0 +1,45 @@
module Handler.Exam.Schedule
( getEScheduleOptSetR, postEScheduleOptSetR
, getEScheduleOptDelR, postEScheduleOptDelR
) where
import Import
import Handler.Utils.Exam
getEScheduleOptSetR, postEScheduleOptSetR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> ExamOccurrenceName -> Bool -> Handler Html
getEScheduleOptSetR = postEScheduleOptSetR
postEScheduleOptSetR tid ssh csh examn eoccn opt = do
uid <- requireAuthId
mResult <- runDB $ maybeT (return Nothing) $ do
eId <- lift $ fetchExamId tid ssh csh examn
eoId <- MaybeT . getKeyBy $ UniqueExamOccurrence eId eoccn
scheduleOpt <- lift $ upsert ExamOccurrenceScheduleOpt
{ examOccurrenceScheduleOptExamOccurrence = eoId
, examOccurrenceScheduleOptUser = uid
, examOccurrenceScheduleOptOpt = opt
}
[ ExamOccurrenceScheduleOptOpt =. opt
]
return $ Just scheduleOpt
case mResult of
Just (Entity _ ExamOccurrenceScheduleOpt{..}) -> addMessageI Success $ bool MsgScheduleOptOutSuccess MsgScheduleOptInSuccess examOccurrenceScheduleOptOpt
Nothing -> addMessageI Error MsgScheduleOptError
redirect $ CExamR tid ssh csh examn EShowR
getEScheduleOptDelR, postEScheduleOptDelR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> ExamOccurrenceName -> Handler Html
getEScheduleOptDelR = postEScheduleOptDelR
postEScheduleOptDelR tid ssh csh examn eoccn = do
uid <- requireAuthId
runDB $ maybeT (return ()) $ do
eId <- lift $ fetchExamId tid ssh csh examn
eoId <- MaybeT . getKeyBy $ UniqueExamOccurrence eId eoccn
lift . deleteBy $ UniqueExamOccurrenceScheduleOpt eoId uid
addMessageI Success MsgScheduleOptDeleteSuccess
redirect $ CExamR tid ssh csh examn EShowR

View File

@ -24,11 +24,14 @@ import qualified Data.CaseInsensitive as CI
import Handler.Utils
import Handler.Utils.Exam
import Utils.Schedule
getEShowR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
getEShowR tid ssh csh examn = do
cTime <- liftIO getCurrentTime
mUid <- maybeAuthId
mAuth <- maybeAuth
ata <- getSessionActiveAuthTags
(Entity eId Exam{..}, School{..}, examParts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, bonus, occurrences, (registered, mayRegister), registeredCount, lecturerInfoShown, staffInfoShown, extraSchools, (partsVisible, partsShown)) <- runDB $ do
exam@(Entity eId Exam{..}) <- fetchExam tid ssh csh examn
@ -54,21 +57,27 @@ getEShowR tid ssh csh examn = do
flip filterM sheets' $ \(Entity _ Sheet{..}) -> hasReadAccessTo $ CSheetR tid ssh csh sheetName SShowR
examParts <- fmap (sortOn . view $ _1 . _entityVal . _examPartNumber) $ selectList [ ExamPartExam ==. eId ] [ Asc ExamPartName ] >>= traverse (\ep@(Entity epId _) -> (ep,,) <$> encrypt @ExamPartId @UUID epId <*> examPartSheets epId)
resultsRaw <- for mUid $ \uid ->
E.select . E.from $ \examPartResult -> do
resultsRaw <- for mAuth $ \(Entity uid _) -> E.select . E.from $ \examPartResult -> do
E.where_ $ examPartResult E.^. ExamPartResultUser E.==. E.val uid
E.&&. examPartResult E.^. ExamPartResultExamPart `E.in_` E.valList (map (views _1 entityKey) examParts)
return examPartResult
let results = maybe Map.empty (\rs -> Map.fromList [ (examPartResultExamPart, res) | res@(Entity _ ExamPartResult{..}) <- rs ]) resultsRaw
result <- fmap join . for mUid $ getBy . UniqueExamResult eId
bonus <- fmap join . for mUid $ getBy . UniqueExamBonus eId
result <- fmap join . for mAuth $ getBy . UniqueExamResult eId . entityKey
bonus <- fmap join . for mAuth $ getBy . UniqueExamBonus eId . entityKey
occurrencesRaw <- E.select . E.from $ \examOccurrence -> do
occurrencesRaw <- E.select . E.from $ \(course `E.InnerJoin` ex `E.InnerJoin` (examOccurrence `E.LeftOuterJoin` examOccurrenceScheduleOpt)) -> do
E.on $ course E.^. CourseId E.==. ex E.^. ExamCourse
E.on $ ex E.^. ExamId E.==. examOccurrence E.^. ExamOccurrenceExam
E.on $ E.just (examOccurrence E.^. ExamOccurrenceId) E.==. examOccurrenceScheduleOpt E.?. ExamOccurrenceScheduleOptExamOccurrence
E.where_ $ examOccurrence E.^. ExamOccurrenceExam E.==. E.val eId
-- TODO: works for now, but can possibly be simplified
E.&&. maybe E.true (\(Entity uid _) -> E.isNothing (examOccurrenceScheduleOpt E.?. ExamOccurrenceScheduleOptUser) E.||. examOccurrenceScheduleOpt E.?. ExamOccurrenceScheduleOptUser E.==. E.just (E.val uid)) mAuth
let
shouldBeDisplayedInSchedule = examOccurrenceShouldBeDisplayedInSchedule (entityKey <$> mAuth) ata cTime course ex examOccurrence
registered
| Just uid <- mUid
| Just (Entity uid _) <- mAuth
= E.exists . E.from $ \examRegistration ->
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eId
E.&&. examRegistration E.^. ExamRegistrationUser E.==. E.val uid
@ -79,22 +88,22 @@ getEShowR tid ssh csh examn = do
= E.subSelectCount . E.from $ \examRegistration ->
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eId
E.&&. examRegistration E.^. ExamRegistrationOccurrence E.==. E.just (examOccurrence E.^. ExamOccurrenceId)
showRoom = maybe E.false (flip showExamOccurrenceRoom examOccurrence . E.val) mUid
showRoom = maybe E.false (flip showExamOccurrenceRoom examOccurrence . E.val . entityKey) mAuth
E.||. E.not_ (examOccurrence E.^. ExamOccurrenceRoomHidden)
E.orderBy [E.desc registered, E.asc $ examOccurrence E.^. ExamOccurrenceStart, E.asc $ examOccurrence E.^. ExamOccurrenceRoom]
return (examOccurrence, registered, registeredCount, showRoom)
return (examOccurrence, registered, registeredCount, showRoom, shouldBeDisplayedInSchedule, examOccurrenceScheduleOpt)
registeredCount <- fromIntegral <$> count [ ExamRegistrationExam ==. eId ]
registered <- for mUid $ getBy . UniqueExamRegistration eId
registered <- for mAuth $ getBy . UniqueExamRegistration eId . entityKey
mayRegister <- if
| examOccurrenceRule == ExamRoomFifo -> anyM occurrencesRaw $ \(Entity _ ExamOccurrence{..}, _, _, _) ->
| examOccurrenceRule == ExamRoomFifo -> anyM occurrencesRaw $ \(Entity _ ExamOccurrence{..}, _, _, _, _, _) ->
hasWriteAccessTo . CExamR tid ssh csh examName $ ERegisterOccR examOccurrenceName
| otherwise -> hasWriteAccessTo $ CExamR tid ssh csh examName ERegisterR
let occurrences = sortOn sortPred $ map (over _4 E.unValue . over _3 E.unValue . over _2 E.unValue) occurrencesRaw
let occurrences = sortOn sortPred $ map (over _5 E.unValue . over _4 E.unValue . over _3 E.unValue . over _2 E.unValue) occurrencesRaw
where
sortPred (Entity _ ExamOccurrence{..}, registered', _, showRoom)
sortPred (Entity _ ExamOccurrence{..}, registered', _, showRoom, _, _)
= (Down $ registered' && not mayRegister, examOccurrenceStart, guardOn @Maybe showRoom examOccurrenceRoom)
staffInfoShown <- hasReadAccessTo $ CExamR tid ssh csh examn EGradesR
@ -133,13 +142,13 @@ getEShowR tid ssh csh examn = do
mayRegister' <- fmap ((Map.!) . Map.fromList) . for (Nothing : map Just occurrences) $ \case
Nothing ->
fmap (Nothing, ) . hasWriteAccessTo $ CExamR tid ssh csh examName ERegisterR
Just (Entity occId ExamOccurrence{..}, _, _, _) ->
Just (Entity occId ExamOccurrence{..}, _, _, _, _, _) ->
fmap (Just occId, ) . hasWriteAccessTo . CExamR tid ssh csh examName $ ERegisterOccR examOccurrenceName
let examTimes = all (\(Entity _ ExamOccurrence{..}, _, _, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences
let examTimes = all (\(Entity _ ExamOccurrence{..}, _, _, _, _, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences
examRoom = do
(Entity _ primeOcc, _, _, _) <- occurrences ^? _head
guard $ all (\(Entity _ occ, _, _, _) -> examOccurrenceRoom occ == examOccurrenceRoom primeOcc) occurrences
(Entity _ primeOcc, _, _, _, _, _) <- occurrences ^? _head
guard $ all (\(Entity _ occ, _, _, _, _, _) -> examOccurrenceRoom occ == examOccurrenceRoom primeOcc) occurrences
guard $ andOf (folded . _4) occurrences
examOccurrenceRoom primeOcc
registerWidget mOcc

View File

@ -24,22 +24,27 @@ import qualified Data.Conduit.Lift as C
import qualified Data.HashMap.Strict as HashMap
-- TODO: deprecated
import Utils.Schedule.Types (ScheduleOffset(..))
import Utils.Schedule.Week
import Handler.Utils.Exam (showExamOccurrenceRoom)
getNewsR :: Handler Html
getNewsR = do
muid <- maybeAuthId
mUser <- maybeAuth
defaultLayout $ do
setTitleI MsgNewsHeading
newsSystemMessages
when (is _Nothing muid) $
when (is _Nothing mUser) $
notificationWidget NotificationBroad Info $(i18nWidgetFile "pitch")
case muid of
Just uid -> do
case mUser of
Just user@(Entity uid _) -> do
newsSchedule user
newsUpcomingExams uid
newsUpcomingSheets uid
Nothing ->
@ -93,6 +98,14 @@ newsSystemMessages = do
$(widgetFile "news/system-messages")
-- TODO: deprecated; update once ScheduleR is finished
newsSchedule :: Entity User -> Widget
newsSchedule user = do
now <- liftIO getCurrentTime
let schedule = weekSchedule now user ScheduleOffsetNone
$(widgetFile "news/schedule")
newsUpcomingSheets :: UserId -> Widget
newsUpcomingSheets uid = do
cTime <- liftIO getCurrentTime

View File

@ -44,8 +44,11 @@ import qualified Data.CaseInsensitive as CI
import Jobs
import Foundation.I18n ()
import Foundation.Yesod.Auth (updateUserLanguage)
import Utils.Schedule.Types.ScheduleView
{-# ANN module ("HLint: ignore Functor law" :: String) #-}
@ -67,12 +70,19 @@ type EOLabels = Map (Either ExamOfficeLabelName ExamOfficeLabelId) EOLabelData
data SettingsForm = SettingsForm
{ stgDisplayName :: UserDisplayName
, stgDisplayEmail :: UserEmail
, stgScheduleView :: ScheduleView
, stgScheduleWeekDays :: ScheduleWeekDays
, stgScheduleWeekTimeFrom
, stgScheduleWeekTimeTo :: NominalDiffTime
, stgScheduleWeekTimeslotLength :: NominalDiffTime
, stgScheduleOccurrenceDisplayDefault :: Bool
, stgMaxFavourites :: Int
, stgMaxFavouriteTerms :: Int
, stgTheme :: Theme
, stgDateTime :: DateTimeFormat
, stgDate :: DateTimeFormat
, stgTime :: DateTimeFormat
, stgWeekStart :: DayOfWeek
, stgDownloadFiles :: Bool
, stgWarningDays :: NominalDiffTime
, stgShowSex :: Bool
@ -122,8 +132,21 @@ makeSettingForm template html = do
-- isAdmin <- checkAdmin
(result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm
<$ aformSection MsgFormPersonalAppearance
<*> areq (textField & cfStrip) (fslI MsgUserDisplayName & setTooltip MsgUserDisplayNameRulesBelow) (stgDisplayName <$> template)
<*> areq (textField & cfStrip) (fslI MsgUserDisplayName & setTooltip MsgUserDisplayNameRulesBelow) (stgDisplayName <$> template)
<*> areq (textField & cfStrip & cfCI) (fslI MsgUserDisplayEmail & setTooltip MsgUserDisplayEmailTip) (stgDisplayEmail <$> template)
<* aformSection MsgSchedule
<*> (case universeF of
[sOpt] -> pure sOpt -- Don't bother showing the select as long as there is only one option
_other -> apopt (selectField optionsFinite) (fslI MsgProfileScheduleView & setTooltip MsgProfileScheduleViewTip) { fsId = Just "schedule-view-select" } (stgScheduleView <$> template)
)
<*> scheduleWeekDaysForm (stgScheduleWeekDays <$> template)
<*> areq timeOfDayField
(fslpI MsgScheduleWeekTimeFrom (mr MsgScheduleWeekTimeFromPlaceholder) & setTooltip MsgScheduleWeekTimeFromTip) (stgScheduleWeekTimeFrom <$> template)
<*> areq timeOfDayField
(fslpI MsgScheduleWeekTimeTo (mr MsgScheduleWeekTimeToPlaceholder ) & setTooltip MsgScheduleWeekTimeToTip ) (stgScheduleWeekTimeTo <$> template)
<*> areq (convertField (fromInteger . (* 60)) ((`quot` 60) . round) $ posIntFieldI MsgScheduleWeekTimeslotLength)
(fslpI MsgScheduleWeekTimeslotLength (mr MsgScheduleWeekTimeslotLengthPlaceholder) & setTooltip MsgScheduleWeekTimeslotLengthTip) (stgScheduleWeekTimeslotLength <$> template)
<*> apopt checkBoxField (fslI MsgScheduleOccurrenceDisplayDefault & setTooltip MsgScheduleOccurrenceDisplayDefaultTip) (stgScheduleOccurrenceDisplayDefault <$> template)
<* aformSection MsgFormCosmetics
<*> areq (natFieldI MsgFavouritesNotNatural)
(fslpI MsgFavourites (mr MsgFavouritesPlaceholder) & setTooltip MsgFavouritesTip) (stgMaxFavourites <$> template)
@ -134,6 +157,7 @@ makeSettingForm template html = do
<*> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) (stgDateTime <$> template)
<*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) (stgDate <$> template)
<*> areq (selectField $ dateTimeFormatOptions SelFormatTime) (fslI MsgTimeFormat) (stgTime <$> template)
<*> areq (selectField optionsFinite) (fslI MsgWeekStart) (stgWeekStart <$> template)
<* aformSection MsgFormBehaviour
<*> apopt checkBoxField (fslI MsgDownloadFiles
& setTooltip MsgDownloadFilesTip
@ -156,7 +180,12 @@ makeSettingForm template html = do
<*> notificationForm (stgNotificationSettings <$> template)
return (result, widget) -- no validation here, done later by validateSettings
where
themeList = [Option (toMessage t) t (toPathPiece t) | t <- universeF]
themeList = [ Option (toMessage t) t (toPathPiece t) | t <- universeF ]
scheduleWeekDaysForm :: Maybe ScheduleWeekDays -> AForm Handler ScheduleWeekDays
scheduleWeekDaysForm template' = prismAForm (_Wrapped . _IndicatorFunction) template' $ \template
-> let dayForm wDay = apopt checkBoxField (fslI wDay) (template <&> ($ wDay))
in funcForm dayForm (fslI MsgScheduleWeekDays & setTooltip MsgScheduleWeekDaysTip) False
schoolsForm :: Maybe (Set SchoolId) -> AForm Handler (Set SchoolId)
schoolsForm template = formToAForm $ schoolsFormView =<< renderWForm FormStandard schoolsForm' mempty
@ -375,6 +404,11 @@ validateSettings User{..} = do
userDisplayName == userDisplayName' || -- unchanged or valid (invalid displayNames delivered by LDAP are preserved)
validDisplayName userTitle userFirstName userSurname userDisplayName'
userScheduleWeekTimeFrom' <- use _stgScheduleWeekTimeFrom
userScheduleWeekTimeTo' <- use _stgScheduleWeekTimeTo
guardValidation MsgScheduleWeekTimeToMustBeAfterTimeFrom
$ userScheduleWeekTimeTo' > userScheduleWeekTimeFrom'
userDisplayEmail' <- use _stgDisplayEmail
guardValidation (MsgInvalidEmailAddressWith $ CI.original userDisplayEmail') $
validEmail' userDisplayEmail' || -- valid
@ -445,12 +479,19 @@ serveProfileR (uid, user@User{..}) = do
let settingsTemplate = Just SettingsForm
{ stgDisplayName = userDisplayName
, stgDisplayEmail = if userDisplayEmail == "" then userEmail else userDisplayEmail
, stgScheduleView = userScheduleView
, stgScheduleWeekDays = userScheduleWeekDays
, stgScheduleWeekTimeFrom = userScheduleWeekTimeFrom
, stgScheduleWeekTimeTo = userScheduleWeekTimeTo
, stgScheduleWeekTimeslotLength = userScheduleWeekTimeslotLength
, stgScheduleOccurrenceDisplayDefault = userScheduleOccurrenceDisplayDefault
, stgMaxFavourites = userMaxFavourites
, stgMaxFavouriteTerms = userMaxFavouriteTerms
, stgTheme = userTheme
, stgDateTime = userDateTimeFormat
, stgDate = userDateFormat
, stgTime = userTimeFormat
, stgWeekStart = userWeekStart
, stgDownloadFiles = userDownloadFiles
, stgSchools = userSchools
, stgNotificationSettings = userNotificationSettings
@ -480,12 +521,19 @@ serveProfileR (uid, user@User{..}) = do
[ UserDisplayEmail =. stgDisplayEmail | not changeEmailByUser, changeEmailProper ] ++ -- DisplayEmail changes by Users must be confirmed, see 480 below
[ UserPostLastUpdate =. Just now | userPostAddress /= stgPostAddress ] ++
[ UserDisplayName =. stgDisplayName
, UserScheduleView =. stgScheduleView
, UserScheduleWeekDays =. stgScheduleWeekDays
, UserScheduleWeekTimeFrom =. stgScheduleWeekTimeFrom
, UserScheduleWeekTimeTo =. stgScheduleWeekTimeTo
, UserScheduleWeekTimeslotLength =. stgScheduleWeekTimeslotLength
, UserScheduleOccurrenceDisplayDefault =. stgScheduleOccurrenceDisplayDefault
, UserMaxFavourites =. stgMaxFavourites
, UserMaxFavouriteTerms =. stgMaxFavouriteTerms
, UserTheme =. stgTheme
, UserDateTimeFormat =. stgDateTime
, UserDateFormat =. stgDate
, UserTimeFormat =. stgTime
, UserWeekStart =. stgWeekStart
, UserDownloadFiles =. stgDownloadFiles
, UserWarningDays =. stgWarningDays
, UserNotificationSettings =. stgNotificationSettings

105
src/Handler/Schedule.hs Normal file
View File

@ -0,0 +1,105 @@
module Handler.Schedule
( getScheduleR, postScheduleR
) where
import Import
import Handler.Utils.Form
import Handler.Utils.I18n
import Utils.Schedule.Types
import Utils.Schedule.Week
getScheduleR, postScheduleR :: Handler Html
getScheduleR = postScheduleR
postScheduleR = do
now <- liftIO getCurrentTime
user@(Entity _uid User{userScheduleView}) <- requireAuth
-- TODO: local instead of global get params?
mOptions <- lookupGlobalGetParam GetScheduleOptions
let
defaultScheduleOptions :: ScheduleOptions
defaultScheduleOptions = ScheduleOptions
{ scheduleView = userScheduleView
, scheduleOffset = ScheduleOffsetNone
, scheduleOptionsAction = ScheduleSetDefault
}
currentScheduleOptions :: ScheduleOptions
currentScheduleOptions = fromMaybe defaultScheduleOptions mOptions
scheduleOptionsForm :: Html -> MForm (HandlerFor UniWorX) (FormResult ScheduleOptions, Widget)
scheduleOptionsForm csrf = do
(viewRess, viewWidgets) <- fmap unzip . for (universeF :: [ScheduleView]) $ \sView ->
mopt (buttonFieldNoParse ScheduleOptions
{ scheduleView = sView
, scheduleOffset = ScheduleOffsetNone
, scheduleOptionsAction = ScheduleSetView
})
("" { fsName = Just $ toPathPiece GetScheduleOptions
, fsAttrs = if sView == scheduleView currentScheduleOptions then [("disabled","")] else mempty
}) Nothing
viewRes <- if
| Just errs <- fromNullable (filter (is _FormFailure) viewRess) -> do
mapM_ formFailure2Alerts errs
(return . FormFailure . mconcat . catMaybes . (fmap $ preview _FormFailure) . toNullable) errs
| otherwise -> case catMaybes $ catMaybes (formResultToMaybe <$> viewRess) of
[ScheduleOptions{scheduleView=sView}] -> (return . FormSuccess) $ ScheduleOptions
{ scheduleView = sView
, scheduleOffset = ScheduleOffsetNone
, scheduleOptionsAction = ScheduleSetView
}
_ -> return $ FormSuccess $ currentScheduleOptions
{ scheduleOffset = ScheduleOffsetNone
, scheduleOptionsAction = ScheduleSetView
}
let
offsetBtns = case viewRes of
FormSuccess ScheduleOptions{scheduleView=ScheduleViewWeek}
-> weekOffsets now user (scheduleOffset currentScheduleOptions)
<&> (\sNewOffset -> currentScheduleOptions
{ scheduleOffset = case sNewOffset of
ScheduleOffsetNone -> ScheduleOffsetNone
_ -> scheduleOffset currentScheduleOptions `addOffset` sNewOffset
, scheduleOptionsAction = ScheduleSetOffset sNewOffset
})
_ -> mempty
(offsetRess, offsetWidgets) <- fmap unzip . for offsetBtns $ \btn ->
mopt (buttonFieldNoParse btn) ("" { fsName = Just $ toPathPiece GetScheduleOptions
, fsAttrs = if ((==) `on `offsetInDays) (scheduleOffset btn) (scheduleOffset currentScheduleOptions)
then [("disabled","")]
else mempty
}) Nothing
offsetRes <- if
| Just errs <- fromNullable (filter (is _FormFailure) offsetRess) -> do
mapM_ formFailure2Alerts errs
(return . FormFailure . mconcat . catMaybes . (fmap $ preview _FormFailure) . toNullable) errs
| otherwise -> case catMaybes $ catMaybes (formResultToMaybe <$> offsetRess) of
[opt] -> return $ FormSuccess opt
_ -> return $ FormSuccess currentScheduleOptions
let
scheduleResult = case (viewRes, offsetRes) of
(_, opts@(FormSuccess _)) -> opts
(opts@(FormSuccess _), _) -> opts
_ -> FormSuccess currentScheduleOptions
optionsWidget = $(widgetFile "schedule/options")
return (scheduleResult, optionsWidget)
((optionsRes, optionsWidget), optionsEnctype) <- runFormGet scheduleOptionsForm
schedule <- case optionsRes of
FormSuccess ScheduleOptions{scheduleView=ScheduleViewWeek,..} -> return $ weekSchedule now user scheduleOffset
other -> formFailure2Alerts other >> return mempty
let scheduleExplanation = $(i18nWidgetFile "schedule-explanation")
siteLayoutMsg MsgMenuSchedule $ do
setTitleI MsgMenuSchedule
$(widgetFile "schedule")

View File

@ -13,5 +13,6 @@ import Handler.Tutorial.Form as Handler.Tutorial
import Handler.Tutorial.List as Handler.Tutorial
import Handler.Tutorial.New as Handler.Tutorial
import Handler.Tutorial.Register as Handler.Tutorial
import Handler.Tutorial.Schedule as Handler.Tutorial
import Handler.Tutorial.TutorInvite as Handler.Tutorial
import Handler.Tutorial.Users as Handler.Tutorial

View File

@ -0,0 +1,40 @@
module Handler.Tutorial.Schedule
( getTScheduleOptSetR, postTScheduleOptSetR
, getTScheduleOptDelR, postTScheduleOptDelR
) where
import Import
import Handler.Utils.Tutorial
getTScheduleOptSetR, postTScheduleOptSetR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Bool -> Handler Html
getTScheduleOptSetR = postTScheduleOptSetR
postTScheduleOptSetR tid ssh csh tutn opt = do
uid <- requireAuthId
runDB $ do
tutid <- fmap entityKey $ fetchTutorial tid ssh csh tutn
void $ upsert TutorialScheduleOpt
{ tutorialScheduleOptTutorial = tutid
, tutorialScheduleOptUser = uid
, tutorialScheduleOptOpt = opt
}
[ TutorialScheduleOptOpt =. opt
]
addMessageI Success $ bool MsgScheduleOptOutSuccess MsgScheduleOptInSuccess opt
redirect $ CourseR tid ssh csh CShowR
getTScheduleOptDelR, postTScheduleOptDelR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
getTScheduleOptDelR = postTScheduleOptDelR
postTScheduleOptDelR tid ssh csh tutn = do
uid <- requireAuthId
runDB $ do
tutid <- fmap entityKey $ fetchTutorial tid ssh csh tutn
deleteBy $ UniqueTutorialScheduleOpt tutid uid
addMessageI Success MsgScheduleOptDeleteSuccess
redirect $ CourseR tid ssh csh CShowR

View File

@ -329,8 +329,6 @@ updateAvsUserByADC newAvsDataContact@(AvsDataContact apid newAvsPersonInfo newAv
let usrId = userAvsUser usravs
usr <- MaybeT $ get usrId
lift $ do -- maybeT no longer needed from here onwards
uuid :: CryptoUUIDUser <- encrypt usrId
$logInfoS "AVS" [st|updateAvsUserByADC: #{tshow uuid}|]
let oldAvsPersonInfo = userAvsLastPersonInfo usravs -- Nothing is ok here
oldAvsFirmInfo = userAvsLastFirmInfo usravs -- Nothing is ok here
oldAvsCardNo = userAvsLastCardNo usravs & fmap Just
@ -382,73 +380,72 @@ updateAvsUserByADC newAvsDataContact@(AvsDataContact apid newAvsPersonInfo newAv
, UserAvsLastCardNo =. newAvsCardNo
]
usr_up2 <- guardMonoidM (oldAvsFirmInfo /= Just newAvsFirmInfo) $ do
-- update company association & supervision
newCompanyEnt@Entity{entityKey=newCompanyId} <- upsertAvsCompany newAvsFirmInfo oldAvsFirmInfo
upsertCompanySuperior newCompanyEnt newAvsFirmInfo oldAvsFirmInfo usrId -- ensure firmInfo superior is supervisor for this user
oldCompanyEnt <- getAvsCompany `traverseJoin` oldAvsFirmInfo
primaryCompanyId <- userCompanyCompany <<$>> getUserPrimaryCompany usrId
let oldCompanyId = entityKey <$> oldCompanyEnt
-- oldCompanyMb = entityVal <$> oldCompanyEnt
-- pst_up = if
-- -- | isNothing oldCompanyMb || oldCompanyId == primaryCompanyId -- refactor could replace next 4 lines
-- -- -> mkUpdate' usr newCompany oldCompanyMb $ CheckUpdate UserPrefersPostal _companyPrefersPostal -- always update if company association is fresh (case should not occur in practice though)
-- | isNothing oldCompanyMb
-- -> mkUpdateDirect usr newCompany $ CheckUpdate UserPrefersPostal _companyPrefersPostal -- always update if company association is fresh (case should not occur in practice though)
-- | oldCompanyId == primaryCompanyId -- && isJust oldCompanyId -- is ensured by previous line
-- -> mkUpdate usr newCompany oldCompanyMb $ CheckUpdate UserPrefersPostal _companyPrefersPostal -- possibly change postal preference
-- | otherwise
-- -> Nothing
superReasonComDef = tshow SupervisorReasonCompanyDefault
newUserComp = UserCompany usrId newCompanyId False False 1 True Nothing -- default value for new company insertion, if no update can be done
-- update company association & supervision
newCompanyEnt@Entity{entityKey=newCompanyId} <- upsertAvsCompany newAvsFirmInfo oldAvsFirmInfo
oldCompanyEnt <- getAvsCompany `traverseJoin` oldAvsFirmInfo
primaryCompanyId <- userCompanyCompany <<$>> getUserPrimaryCompany usrId
let oldCompanyId = entityKey <$> oldCompanyEnt
-- oldCompanyMb = entityVal <$> oldCompanyEnt
-- pst_up = if
-- -- | isNothing oldCompanyMb || oldCompanyId == primaryCompanyId -- refactor could replace next 4 lines
-- -- -> mkUpdate' usr newCompany oldCompanyMb $ CheckUpdate UserPrefersPostal _companyPrefersPostal -- always update if company association is fresh (case should not occur in practice though)
-- | isNothing oldCompanyMb
-- -> mkUpdateDirect usr newCompany $ CheckUpdate UserPrefersPostal _companyPrefersPostal -- always update if company association is fresh (case should not occur in practice though)
-- | oldCompanyId == primaryCompanyId -- && isJust oldCompanyId -- is ensured by previous line
-- -> mkUpdate usr newCompany oldCompanyMb $ CheckUpdate UserPrefersPostal _companyPrefersPostal -- possibly change postal preference
-- | otherwise
-- -> Nothing
superReasonComDef = tshow SupervisorReasonCompanyDefault
newUserComp = UserCompany usrId newCompanyId False False 1 True Nothing -- default value for new company insertion, if no update can be done
case oldAvsFirmInfo of
_ | Just newCompanyId == oldCompanyId -- company unchanged entirely
-> return mempty -- => do nothing
(Just oafi) | isJust (view _avsFirmPostAddressSimple oafi)
&& ((==) `on` view _avsFirmPostAddressSimple) oafi newAvsFirmInfo -- non-empty company address unchanged OR
|| isJust (view _avsFirmPrimaryEmail oafi)
&& ((==) `on` view _avsFirmPrimaryEmail) oafi newAvsFirmInfo -- non-empty company primary email unchanged
-> do -- => just update user company association, keeping supervision privileges
case oldCompanyId of
Nothing -> void $ insertUnique newUserComp -- it's ok if this already exists
Just ocid -> do
void $ upsertBySafe (UniqueUserCompany usrId ocid) newUserComp (_userCompanyCompany .~ newCompanyId) -- keep default supervisor settings
void $ updateWhere [ UserSupervisorSupervisor ==. usrId -- update company-related supervisions
, UserSupervisorCompany ==. Just ocid -- to new company, regardless of
, UserSupervisorReason ==. Just superReasonComDef] -- user
[ UserSupervisorCompany =. Just newCompanyId]
return mempty
_ | Just newCompanyId == primaryCompanyId -- old primaryCompany is now also AVS-company
-> do
whenIsJust oldCompanyId $ \oldCid -> do
deleteBy $ UniqueUserCompany usrId oldCid
deleteWhere $ (UserSupervisorUser ==. usrId):(UserSupervisorCompany ==. oldCompanyId):(UserSupervisorReason ~=. superReasonComDef)
return mempty
_ -- company changed completely
-> do
(pst_up, problems) <- switchAvsUserCompany False False usrId newCompanyId
mapM_ reportAdminProblem problems
-- Following line does not type, hence additional parameter needed
-- return [ u | u@Update{updateField=f} <- pst_up, f /= UserPostAddress ] -- already computed in frm_up above, duplicate update must be prevented (version above accounts for legacy updates)
return pst_up
-- SPECIALISED CODE, PROBABLY DEPRECATED
-- switch user company, keeping old priority
-- (getBy . UniqueUserCompany usrId) `traverseJoin` oldCompanyId >>= \case
-- Nothing ->
-- void $ insertUnique newUserComp
-- Just Entity{entityKey=ucidOld, entityVal=UserCompany{userCompanyCompany, userCompanySupervisor, userCompanySupervisorReroute, userCompanyPriority}} -> do
-- when userCompanySupervisor $ reportAdminProblem $ AdminProblemSupervisorNewCompany usrId userCompanyCompany newCompanyId userCompanySupervisorReroute
-- delete ucidOld
-- void $ insertUnique newUserComp{userCompanyPriority} -- keep priority, if insert succeeds
-- -- adjust supervison
-- let oldCompDefSuperFltr = mconcat [UserSupervisorCompany ~~. oldCompanyId, UserSupervisorReason ~=. superReasonComDef]
-- deleteWhere $ (UserSupervisorSupervisor ==. usrId) : oldCompDefSuperFltr
-- oldAPs <- deleteWhereCount $ (UserSupervisorUser ==. usrId) : oldCompDefSuperFltr
-- addDefaultSupervisors' newCompanyId $ singleton usrId
-- newAPs <- count $ (UserSupervisorUser ==. usrId) : (UserSupervisorCompany ==. Just newCompanyId) : (UserSupervisorReason ~=. superReasonComDef)
-- when (oldAPs > 0 && newAPs <= 0) $ reportAdminProblem $ AdminProblemNewlyUnsupervised usrId oldCompanyId newCompanyId
-- return pst_up
usr_up2 <- case oldAvsFirmInfo of
_ | Just newCompanyId == oldCompanyId -- company unchanged entirely
-> return mempty -- => do nothing
(Just oafi) | isJust (view _avsFirmPostAddressSimple oafi)
&& ((==) `on` view _avsFirmPostAddressSimple) oafi newAvsFirmInfo -- non-empty company address unchanged OR
|| isJust (view _avsFirmPrimaryEmail oafi)
&& ((==) `on` view _avsFirmPrimaryEmail) oafi newAvsFirmInfo -- non-empty company primary email unchanged
-> do -- => just update user company association, keeping supervision privileges
case oldCompanyId of
Nothing -> void $ insertUnique newUserComp -- it's ok if this already exists
Just ocid -> do
void $ upsertBySafe (UniqueUserCompany usrId ocid) newUserComp (_userCompanyCompany .~ newCompanyId) -- keep default supervisor settings
void $ updateWhere [ UserSupervisorSupervisor ==. usrId -- update company-related supervisions
, UserSupervisorCompany ==. Just ocid -- to new company, regardless of
, UserSupervisorReason ==. Just superReasonComDef] -- user
[ UserSupervisorCompany =. Just newCompanyId]
return mempty
_ | Just newCompanyId == primaryCompanyId -- old primaryCompany is now also AVS-company
-> do
whenIsJust oldCompanyId $ \oldCid -> do
deleteBy $ UniqueUserCompany usrId oldCid
deleteWhere $ (UserSupervisorUser ==. usrId):(UserSupervisorCompany ==. oldCompanyId):(UserSupervisorReason ~=. superReasonComDef)
return mempty
_ -- company changed completely
-> do
(pst_up, problems) <- switchAvsUserCompany False False usrId newCompanyId
mapM_ reportAdminProblem problems
-- Following line does not type, hence additional parameter needed
-- return [ u | u@Update{updateField=f} <- pst_up, f /= UserPostAddress ] -- already computed in frm_up above, duplicate update must be prevented (version above accounts for legacy updates)
return pst_up
-- SPECIALISED CODE, PROBABLY DEPRECATED
-- switch user company, keeping old priority
-- (getBy . UniqueUserCompany usrId) `traverseJoin` oldCompanyId >>= \case
-- Nothing ->
-- void $ insertUnique newUserComp
-- Just Entity{entityKey=ucidOld, entityVal=UserCompany{userCompanyCompany, userCompanySupervisor, userCompanySupervisorReroute, userCompanyPriority}} -> do
-- when userCompanySupervisor $ reportAdminProblem $ AdminProblemSupervisorNewCompany usrId userCompanyCompany newCompanyId userCompanySupervisorReroute
-- delete ucidOld
-- void $ insertUnique newUserComp{userCompanyPriority} -- keep priority, if insert succeeds
-- -- adjust supervison
-- let oldCompDefSuperFltr = mconcat [UserSupervisorCompany ~~. oldCompanyId, UserSupervisorReason ~=. superReasonComDef]
-- deleteWhere $ (UserSupervisorSupervisor ==. usrId) : oldCompDefSuperFltr
-- oldAPs <- deleteWhereCount $ (UserSupervisorUser ==. usrId) : oldCompDefSuperFltr
-- addDefaultSupervisors' newCompanyId $ singleton usrId
-- newAPs <- count $ (UserSupervisorUser ==. usrId) : (UserSupervisorCompany ==. Just newCompanyId) : (UserSupervisorReason ~=. superReasonComDef)
-- when (oldAPs > 0 && newAPs <= 0) $ reportAdminProblem $ AdminProblemNewlyUnsupervised usrId oldCompanyId newCompanyId
-- return pst_up
upsertCompanySuperior newCompanyEnt newAvsFirmInfo oldAvsFirmInfo [usrId] -- ensure firmInfo superior is supervisor for this user
update usrId usr_up2 -- update user by company switch first (due to possible conflicts with usr_up2)
update usrId usr_up1 -- update user eventually
update uaId avs_ups -- update stored avsinfo for future updates
@ -588,18 +585,16 @@ getAvsCompany afi =
-- | insert a company from AVS firm info or update an existing one based on previous values
upsertAvsCompany :: AvsFirmInfo -> Maybe AvsFirmInfo -> DB (Entity Company)
-- upsertAvsCompany newAvsFirmInfo (Just oldAvsFirmInfo)
-- | newAvsFirmInfo == oldAvsFirmInfo = maybeM (upsertAvsCompany newAvsFirmInfo Nothing) pure $ getAvsCompany newAvsFirmInfo -- firmInfo unchanged, shortcircuit; SHORTCIRCUIT no longer needed, checked at call-site due to result not being wrapped in Maybe
upsertAvsCompany newAvsFirmInfo mbOldAvsFirmInfo = do
mbFirmEnt <- getAvsCompany newAvsFirmInfo -- primarily by AvsId, then Shorthand, then name
$logInfoS "AVS" [st|upsertAvsCompany: old #{tshow mbOldAvsFirmInfo} new #{tshow newAvsFirmInfo} ent-new #{tshow mbFirmEnt}|]
case mbFirmEnt of
Nothing -> do -- insert new company, neither AvsId nor Shorthand exist in DB
$logInfoS "AVS" [st|upsertAvsCompany: old #{tshow mbFirmEnt} new #{tshow newAvsFirmInfo}|]
case (mbFirmEnt, mbOldAvsFirmInfo) of
(Nothing, _) -> do -- insert new company, neither AvsId nor Shorthand exist in DB
afn <- if 0 < newAvsFirmInfo ^. _avsFirmFirmNo
then return $ newAvsFirmInfo ^. _avsFirmFirmNo
else maybe (-1) (pred . companyAvsId . entityVal) <$> selectMaybe [CompanyAvsId <. 0] [Asc CompanyAvsId]
let upd = flip updateRecord newAvsFirmInfo
dmy = Company -- mostly dummy, values are actually produced through firmInfo2company below for consistency
dmy = Company -- mostly dummy, values are actually prodcued through firmInfo2company below for consistency
{ companyName = newAvsFirmInfo ^. _avsFirmFirm . from _CI
, companyShorthand = newAvsFirmInfo ^. _avsFirmAbbreviation . from _CI
, companyAvsId = afn
@ -611,12 +606,11 @@ upsertAvsCompany newAvsFirmInfo mbOldAvsFirmInfo = do
$logInfoS "AVS" $ "Insert new company: " <> tshow cmp
newCmp <- insertEntity cmp
reportAdminProblem $ AdminProblemNewCompany $ entityKey newCmp
$logInfoS "AVS" "Insert new company completed."
return newCmp
(Just Entity{entityKey=firmid, entityVal=firm}) -> do -- possibly update existing company, if isJust oldAvsFirmInfo and identical AvsFirmNo and changes occurred
let oldHasSameFirmNo = Just (newAvsFirmInfo ^. _avsFirmFirmNo) == (mbOldAvsFirmInfo ^? _Just . _avsFirmFirmNo)
oldAvsFirmInfo = guardOnM oldHasSameFirmNo mbOldAvsFirmInfo
cmp_ups = mapMaybe (mkUpdate' firm newAvsFirmInfo oldAvsFirmInfo) firmInfo2company
(Just Entity{entityKey=firmid, entityVal=firm}, oldAvsFirmInfo) -> do -- possibly update existing company, if isJust oldAvsFirmInfo and changed occurred
let cmp_ups = mapMaybe (mkUpdate' firm newAvsFirmInfo oldAvsFirmInfo) firmInfo2company
key_ups = mkUpdate' firm newAvsFirmInfo oldAvsFirmInfo firmInfo2key
uniq_ups <- mkUpdateCheckUnique' firm newAvsFirmInfo oldAvsFirmInfo firmInfo2companyNo
$logInfoS "AVS" [st|Update company #{companyShorthand firm}: #{tshow (length cmp_ups)}, #{tshow (length key_ups)}, #{tshow (length uniq_ups)} for #{tshow oldAvsFirmInfo}|]
@ -635,7 +629,7 @@ upsertAvsCompany newAvsFirmInfo mbOldAvsFirmInfo = do
| otherwise -> $logInfoS "AVS" $ "Update company shorthand failed for " <> ciOriginal cmp_key <> " and " <> ciOriginal alt_key
maybeM (return res_cmp) return $ getBy uniq_cmp
_otherwise -> return res_cmp
$logInfoS "AVS" [st|Update company #{companyShorthand firm} completed.|]
$logInfoS "AVS" "Update company completed."
return res_cmp2
where
firmInfo2key =
@ -651,8 +645,8 @@ upsertAvsCompany newAvsFirmInfo mbOldAvsFirmInfo = do
-- | adjust superiors, assumes that CompanyUser exists for all usrs for given company; does not work otherwise
upsertCompanySuperior :: Entity Company -> AvsFirmInfo -> Maybe AvsFirmInfo -> UserId -> DB () -- may return superior (Maybe UserId), but currently not needed
upsertCompanySuperior Entity{entityKey=cid, entityVal=Company{}} newAfi oldAfi usrId =
upsertCompanySuperior :: Entity Company -> AvsFirmInfo -> Maybe AvsFirmInfo -> [UserId] -> DB () -- may return superior (Maybe UserId), but currently not needed
upsertCompanySuperior Entity{entityKey=cid, entityVal=Company{}} newAfi oldAfi usrs =
let reasonSuperior = Just $ tshow SupervisorReasonAvsSuperior
getInsertUid eml = altM (guessUserByEmail $ stripCI eml) (catchAll2log' $ Just . entityKey <$> ldapLookupAndUpsert eml)
newAvsNo = newAfi ^. _avsFirmFirmNo
@ -661,26 +655,22 @@ upsertCompanySuperior Entity{entityKey=cid, entityVal=Company{}} newAfi oldAfi u
mbOldEmail = oldAfi ^? _Just . _avsFirmEMailSuperior . _Just
getSupId = getInsertUid `traverseJoin` mbSupEmail
getOldId = (guessUserByEmail . stripCI) `traverseJoin` mbOldEmail
getSupervision :: Maybe UserId -> DB (Maybe (Entity UserSupervisor))
getSupervision = traverseJoin (getBy . flip UniqueUserSupervisor usrId)
unchangedCompany = oldAvsNo == Just newAvsNo
changedSuperior = mbSupEmail /= mbOldEmail -- beware we only have AvsFirmInfo for one user; also both could be Nothing
unchangedCompany = oldAvsNo == Just newAvsNo
changedSuperior = mbSupEmail /= mbOldEmail -- beware, both could be Nothing
-- 1. not unchangedCompany: do not delete, but ensure that superior supervision is set, since it could be a just a single user company change
-- 2. unchangedCompany && not changedSuperior: superior must already been set, short-circuit
-- 3. unchangedCompany && changedSuperior: update superior for all users
in unless (unchangedCompany && not changedSuperior) $ do -- do nothing if (unchangedCompany && not changedSuperior).
mbSupId <- getSupId
mbUsrSup <- getSupervision mbSupId
mbSupId <- getSupId
-- delete old superiors, if any
when (unchangedCompany && changedSuperior) $
deleteWhere $ mcons ((UserSupervisorSupervisor !=.) <$> mbSupId)
[ UserSupervisorCompany ==. Just cid, UserSupervisorReason ==. reasonSuperior ]
unless unchangedCompany $
deleteWhere [ UserSupervisorReason ==. reasonSuperior, UserSupervisorUser ==. usrId ]
deleteWhere [ UserSupervisorReason ==. reasonSuperior, UserSupervisorUser <-. usrs ]
-- ensure superior supervision
case (mbSupId, mbUsrSup) of
(_ , Just _) -> return () -- supId is already supervisor for uid for any reason
(Just supId, Nothing) -> do
case mbSupId of
Just supId -> do
-- ensure association between company and superior at equal-to-top priority
prio <- getCompanyUserMaxPrio supId
void $ insertUnique (UserCompany supId cid False False prio True reasonSuperior) -- superior is not a supervisor, do not change existing user company associations
@ -712,7 +702,7 @@ upsertCompanySuperior Entity{entityKey=cid, entityVal=Company{}} newAfi oldAfi u
when (unchangedCompany && changedSuperior) $ do
oldSupId <- getOldId
reportAdminProblem $ AdminProblemCompanySuperiorChange supId cid oldSupId
(Nothing, Nothing) ->
Nothing ->
when (unchangedCompany && changedSuperior) $ do
oldSupId <- getOldId
reportAdminProblem $ AdminProblemCompanySuperiorNotFound mbSupEmail cid oldSupId

View File

@ -183,6 +183,16 @@ instance Button UniWorX ButtonSubmitDelete where
nullaryPathPiece ''ButtonSubmitDelete $ camelToPathPiece' 1 . dropSuffix "'"
-- TODO: move
data ButtonScheduleOpt = BtnScheduleOptIn | BtnScheduleOptOut | BtnScheduleOptDel
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
instance Universe ButtonScheduleOpt
instance Finite ButtonScheduleOpt
nullaryPathPiece ''ButtonScheduleOpt $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''ButtonScheduleOpt id
instance Button UniWorX ButtonScheduleOpt where
btnClasses = const [BCIsButton, BCPrimary]
-- | Looks like a button, but is just a link (e.g. for create course, etc.)
linkButton :: Widget -- ^ Widget to display if unauthorized
-> Widget -- ^ Button label
@ -1389,7 +1399,7 @@ dayTimeField fs mutc = do
fieldTimeFormat :: String
-- fieldTimeFormat = "%e.%m.%y %k:%M"
fieldTimeFormat = "%Y-%m-%dT%H:%M:%S"
fieldTimeFormat = "%Y-%m-%dT%H:%M:%S%Q"
localTimeField :: (MonadHandler m, HandlerSite m ~ UniWorX) => Field m LocalTime
localTimeField = Field

View File

@ -48,7 +48,7 @@ occurrencesAForm (toPathPiece -> miIdent') mPrev = wFormToAForm $ do
(\p -> Just . SomeRoute $ cRoute :#: p)
miLayout'
(miIdent' <> "__scheduled" :: Text)
(fslI MsgScheduleRegular)
(fslI MsgScheduleRegular & setTooltip MsgScheduleRegularTip)
False
(Set.toList . occurrencesScheduled <$> mPrev)
where

View File

@ -122,11 +122,11 @@ dispatchJobSynchroniseAvsQueue = JobHandlerException $ do
-- E.truncateTable $ AvsSync (error "truncateTable: AvsSyncUser not needed") now Nothing
-- return jobs
let (unlinked, linked) = foldl' discernJob mempty jobs
$logInfoS "SynchronisAvs" [st|AVS synch start for #{length linked} AVS linked users and #{length unlinked} unlinked users|]
$logInfoS "SynchronisAvs" [st|AVS synch performing for #{length linked} AVS linked users and #{length unlinked} unlinked users|]
void $ updateAvsUserByIds linked
void $ linktoAvsUserByUIDs unlinked
runDB $ deleteWhere [AvsSyncUser <-. (E.unValue . fst3 <$> jobs)]
$logInfoS "SynchronisAvs" [st|AVS synch end for #{length linked} AVS linked users and #{length unlinked} unlinked users|]
$logInfoS "SynchronisAvs" [st|AVS synch performed for #{length linked} AVS linked users and #{length unlinked} unlinked users|]
-- we do not reschedule failed synchs here in order to avoid a loop
where
discernJob accs ( _ , E.Value (Just api), E.Value True ) = accs & over _2 (Set.insert api)

View File

@ -25,6 +25,8 @@ import Settings.Cluster (ClusterSettingsKey)
import Text.Blaze (ToMarkup(..))
import Utils.Schedule.Types.ScheduleView
import Database.Persist.Sql (BackendKey(..))
import qualified Database.Esqueleto.Legacy as E

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -47,14 +47,18 @@ type Companies = [CI Text]
type CourseName = CI Text
type CourseShorthand = CI Text
type CourseEventType = CI Text
type CourseEventRoom = Text
type MaterialName = CI Text
type TutorialName = CI Text
type TutorialType = CI Text
type SheetName = CI Text
type SubmissionGroupName = CI Text
type ExamName = CI Text
type ExamPartName = CI Text
type ExamOccurrenceName = CI Text
type ExamOccurrenceRoom = Text
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -34,6 +34,8 @@ import Web.HttpApiData
import Data.Aeson.Types as Aeson
import Model.Types.TH.PathPiece
----
-- Terms and anything loosely related to time
@ -227,6 +229,18 @@ derivePersistFieldJSON ''Occurrences
nullaryPathPiece ''DayOfWeek camelToPathPiece
derivePersistFieldPathPiece ''DayOfWeek
newtype ScheduleWeekDays = ScheduleWeekDays (Set DayOfWeek)
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving newtype (Semigroup, Monoid)
instance NFData ScheduleWeekDays
deriveJSON defaultOptions ''ScheduleWeekDays
derivePersistFieldJSON ''ScheduleWeekDays
makeWrapped ''ScheduleWeekDays
-- test :: IO [OccurrenceException]

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -79,6 +79,8 @@ import qualified Network.Minio as Minio
import Data.Conduit.Algorithms.FastCDC
import Utils.Schedule.Types.ScheduleView
import Utils.Lens.TH
import qualified Data.Set as Set
@ -282,9 +284,15 @@ data UserDefaultConf = UserDefaultConf
{ userDefaultTheme :: Theme
, userDefaultMaxFavourites, userDefaultMaxFavouriteTerms :: Int
, userDefaultDateTimeFormat, userDefaultDateFormat, userDefaultTimeFormat :: DateTimeFormat
, userDefaultWeekStart :: DayOfWeek
, userDefaultDownloadFiles :: Bool
, userDefaultWarningDays :: NominalDiffTime
, userDefaultShowSex :: Bool
, userDefaultScheduleView :: ScheduleView
, userDefaultScheduleWeekDays :: ScheduleWeekDays
, userDefaultScheduleWeekTimeFrom, userDefaultScheduleWeekTimeTo :: NominalDiffTime
, userDefaultScheduleWeekTimeslotLength :: NominalDiffTime
, userDefaultScheduleOccurrenceDisplayDefault :: Bool
, userDefaultExamOfficeGetSynced :: Bool
, userDefaultExamOfficeGetLabels :: Bool
, userDefaultPrefersPostal :: Bool

View File

@ -831,6 +831,21 @@ listBracket b@(s,e) (h:t)
----------
-- all functions that used to be here are now in Utils.Set
funcFromSet :: Ord k => Set k -> (k -> Bool)
funcFromSet = flip Set.member
_IndicatorFunction :: (Finite k, Ord k) => Iso' (Set k) (k -> Bool)
_IndicatorFunction = iso funcFromSet setFromFunc
setFromMap :: Map k Bool -> Set k
setFromMap = Map.keysSet . Map.filter id
mapFromSet :: Set k -> Map k Bool
mapFromSet = Map.fromSet $ const True
_IndicatorMap :: Iso' (Set k) (Map k Bool)
_IndicatorMap = iso mapFromSet setFromMap
----------
-- Maps --
----------

View File

@ -16,6 +16,7 @@ module Utils.DateTime
, mkDateTimeFormatter
, nominalHour, nominalMinute
, minNominalYear, avgNominalYear
, nominalTimeToTimeOfDay, timeOfDayToNominalTime
, diffMinute, diffHour, diffDay
, module Zones
, day
@ -33,6 +34,7 @@ import Data.Time.Zones (localTimeToUTCTZ, timeZoneForUTCTime)
import Data.Time.Format (FormatTime)
import Data.Time.Format.Instances ()
import Data.Time.Clock.System (systemEpochDay)
import Data.Time.LocalTime (timeToTimeOfDay, timeOfDayToTime)
import qualified Data.Time.Format.ISO8601 as Time
import qualified Data.Time.Format as Time
-- import Data.Time.Calendar (CalendarDiffDays, calendarMonth, scaleCalendarDiffDays)
@ -162,6 +164,11 @@ minNominalYear, avgNominalYear :: NominalDiffTime
minNominalYear = 365 * nominalDay
avgNominalYear = fromRational $ 365.2425 * toRational nominalDay
nominalTimeToTimeOfDay :: NominalDiffTime -> TimeOfDay
nominalTimeToTimeOfDay = timeToTimeOfDay . realToFrac
timeOfDayToNominalTime :: TimeOfDay -> NominalDiffTime
timeOfDayToNominalTime = realToFrac . timeOfDayToTime
--------------
-- DiffTime --
--------------

View File

@ -425,6 +425,27 @@ buttonField btn = Field{..}
| otherwise = return . Left $ SomeMessage MsgWrongButtonValue
fieldParse _ _ = return . Left $ SomeMessage MsgMultipleButtonValues
-- | Similar to buttonField, but with dummy fieldParse
buttonFieldNoParse :: forall a m.
( Button (HandlerSite m) a
, MonadHandler m
) => a -> Field m a
buttonFieldNoParse btn = Field{..}
where
fieldEnctype = UrlEncoded
fieldView :: FieldViewFunc m a
fieldView fid name attrs _val _ = let
validate = btnValidate (Proxy @(HandlerSite m)) btn
classes :: [ButtonClass (HandlerSite m)]
classes = btnClasses btn
in [whamlet|
$newline never
<button class=#{unwords $ map toPathPiece classes} type=submit name=#{name} value=#{toPathPiece btn} *{attrs} ##{fid} :not validate:formnovalidate>^{btnLabel btn}
|]
fieldParse _ _ = return . Right $ Just btn
combinedButtonField :: forall a m.
( Button (HandlerSite m) a
, MonadHandler m
@ -811,6 +832,28 @@ daysField = convertField fromDays toDays fractionalField
toDays = (/ nominalDay)
fromDays = (* nominalDay)
timeOfDayField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m NominalDiffTime
timeOfDayField = Field{..}
where
precision :: Pico
precision = MkFixed 1
timeFormat :: String
timeFormat = "%H:%M:%S%Q"
fieldEnctype = UrlEncoded
fieldView theId name attrs val' isReq
= [whamlet|
$newline never
<input id=#{theId} name=#{name} *{attrs} type=time step=#{showFixed True precision} :isReq:required value=#{val}>
|]
where val :: Text
val = either id (pack . formatTime defaultTimeLocale timeFormat . nominalTimeToTimeOfDay) val'
fieldParse = parseHelper $ \t
-> case parseTimeM True defaultTimeLocale timeFormat (T.unpack t) of
Just tod -> Right $ timeOfDayToNominalTime tod
Nothing -> Left MsgInvalidTimeFormat
data SecretJSONFieldException = SecretJSONFieldDecryptFailure
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
@ -1501,7 +1544,7 @@ hoistField f Field{..} = Field
}
prismAForm :: Monad m => Prism' s a -> Maybe s -> (Maybe a -> AForm m a) -> AForm m s
-- ^ @Monad m => Prism' s a -> (Maybe a -> AForm m a) -> (Maybe s -> AForm m s)@
-- ^ TODO: @Monad m => Prism' s a -> (Maybe a -> AForm m a) -> (Maybe s -> AForm m s)@
prismAForm p outer form = review p <$> form inner
where
inner = outer >>= preview p

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-23 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -96,6 +96,11 @@ data Icon
| IconFileUploadSession
| IconStandaloneFieldError
| IconFileUser
| IconFastBackward
| IconBackward
| IconCurrent
| IconForward
| IconFastForward
| IconPersonalIdentification
| IconMenuWorkflows
| IconVideo
@ -194,6 +199,11 @@ iconText = \case
IconFileUploadSession -> "file-upload"
IconStandaloneFieldError -> "exclamation"
IconFileUser -> "file-user"
IconFastBackward -> "angle-double-left"
IconBackward -> "angle-left"
IconCurrent -> "circle"
IconForward -> "angle-right"
IconFastForward -> "angle-double-right"
IconNotification -> "envelope"
IconNotificationSent -> "envelope-open" -- "paper-plane", "shipping-fast", "hourglass-half"
IconNoNotification -> "bell-slash"

View File

@ -37,6 +37,7 @@ data GlobalGetParam = GetLang
| GetError
| GetSelectTable
| GetGenerateToken
| GetScheduleOptions
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
deriving anyclass (Universe, Finite)

128
src/Utils/Schedule.hs Normal file
View File

@ -0,0 +1,128 @@
module Utils.Schedule
( fetchCourseEventsScheduleInfo, fetchTutorialsScheduleInfo, fetchExamOccurrencesScheduleInfo
, courseEventShouldBeDisplayedInSchedule, tutorialShouldBeDisplayedInSchedule, examOccurrenceShouldBeDisplayedInSchedule
) where
import Import
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import Handler.Utils.Course (showCourseEventRoom)
import Handler.Utils.Exam (showExamOccurrenceRoom)
import Handler.Utils.Tutorial (showTutorialRoom)
import Utils.Course
import Utils.Tutorial
import Utils.Schedule.Types
fetchCourseEventsScheduleInfo :: MonadHandler m => Maybe UserId -> AuthTagActive -> UTCTime -> ReaderT SqlBackend m [ScheduleCourseEventInfo]
fetchCourseEventsScheduleInfo muid ata now = E.select $ E.from $ \(course `E.InnerJoin` courseEvent) -> do
E.on $ course E.^. CourseId E.==. courseEvent E.^. CourseEventCourse
E.where_ $ courseEventShouldBeDisplayedInSchedule muid ata course courseEvent
E.&&. mayViewCourse muid ata now course Nothing -- should not be necessary, but let's be on the safe side
let showRoom = maybe E.false (flip showCourseEventRoom courseEvent . E.val) muid
E.||. E.not_ (courseEvent E.^. CourseEventRoomHidden)
return (course, courseEvent, showRoom)
fetchTutorialsScheduleInfo :: MonadHandler m => Maybe UserId -> AuthTagActive -> UTCTime -> ReaderT SqlBackend m [ScheduleTutorialInfo]
fetchTutorialsScheduleInfo muid ata now = E.select $ E.from $ \(course `E.InnerJoin` tutorial) -> do
E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
E.where_ $ tutorialShouldBeDisplayedInSchedule muid ata course tutorial
E.&&. mayViewCourse muid ata now course Nothing -- should not be necessary, but let's be on the safe side
let showRoom = maybe E.false (flip showTutorialRoom tutorial . E.val) muid
E.||. E.not_ (tutorial E.^. TutorialRoomHidden)
return (course, tutorial, showRoom)
fetchExamOccurrencesScheduleInfo :: MonadHandler m => Maybe UserId -> AuthTagActive -> UTCTime -> ReaderT SqlBackend m [ScheduleExamOccurrenceInfo]
fetchExamOccurrencesScheduleInfo muid ata now = E.select $ E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examOccurrence) -> do
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.on $ exam E.^. ExamId E.==. examOccurrence E.^. ExamOccurrenceExam
E.where_ $ examOccurrenceShouldBeDisplayedInSchedule muid ata now course exam examOccurrence
let showRoom = maybe E.false (flip showExamOccurrenceRoom examOccurrence . E.val) muid
E.||. E.not_ (examOccurrence E.^. ExamOccurrenceRoomHidden)
return (course, exam, examOccurrence, showRoom)
courseEventShouldBeDisplayedInSchedule :: Maybe UserId -> AuthTagActive -> E.SqlExpr (Entity Course) -> E.SqlExpr (Entity CourseEvent) -> E.SqlExpr (E.Value Bool)
courseEventShouldBeDisplayedInSchedule muid@(Just uid) ata course courseEvent = E.exists . E.from $ \user ->
let
mCourseEventOpt = E.subSelect . E.from $ \courseEventScheduleOpt -> do
E.where_ $ courseEventScheduleOpt E.^. CourseEventScheduleOptCourseEvent E.==. courseEvent E.^. CourseEventId
E.&&. courseEventScheduleOpt E.^. CourseEventScheduleOptUser E.==. user E.^. UserId
return $ courseEventScheduleOpt E.^. CourseEventScheduleOptOpt
mCourseOpt = E.subSelect $ getCourseScheduleOpt course user
in E.where_ $ user E.^. UserId E.==. E.val uid
E.&&. E.fromMaybe
( E.fromMaybe
( user E.^. UserScheduleOccurrenceDisplayDefault
E.&&. ( isCourseParticipant muid ata (course E.^. CourseId)
E.||. isCourseLecturer muid ata (course E.^. CourseId)
)
)
mCourseOpt
)
mCourseEventOpt
courseEventShouldBeDisplayedInSchedule _ _ _ _ = E.false
tutorialShouldBeDisplayedInSchedule :: Maybe UserId -> AuthTagActive -> E.SqlExpr (Entity Course) -> E.SqlExpr (Entity Tutorial) -> E.SqlExpr (E.Value Bool)
tutorialShouldBeDisplayedInSchedule muid@(Just uid) ata course tutorial = E.exists . E.from $ \user ->
let
mTutorialOpt = E.subSelect . E.from $ \tutorialScheduleOpt -> do
E.where_ $ tutorialScheduleOpt E.^. TutorialScheduleOptTutorial E.==. tutorial E.^. TutorialId
E.&&. tutorialScheduleOpt E.^. TutorialScheduleOptUser E.==. user E.^. UserId
return $ tutorialScheduleOpt E.^. TutorialScheduleOptOpt
mCourseOpt = E.subSelect $ getCourseScheduleOpt course user
in E.where_ $ user E.^. UserId E.==. E.val uid
E.&&. E.fromMaybe
( ( E.fromMaybe
(user E.^. UserScheduleOccurrenceDisplayDefault)
mCourseOpt
) E.&&. ( isTutorialTutor muid ata (tutorial E.^. TutorialId)
E.||. isTutorialParticipant muid ata (tutorial E.^. TutorialId)
)
)
mTutorialOpt
tutorialShouldBeDisplayedInSchedule _ _ _ _ = E.false
examOccurrenceShouldBeDisplayedInSchedule :: Maybe UserId -> AuthTagActive -> UTCTime -> E.SqlExpr (Entity Course) -> E.SqlExpr (Entity Exam) -> E.SqlExpr (Entity ExamOccurrence) -> E.SqlExpr (E.Value Bool)
examOccurrenceShouldBeDisplayedInSchedule muid@(Just uid) ata now course exam examOcc = E.exists . E.from $ \user ->
let
mExamOccOpt = E.subSelect . E.from $ \examOccScheduleOpt -> do
E.where_ $ examOccScheduleOpt E.^. ExamOccurrenceScheduleOptExamOccurrence E.==. examOcc E.^. ExamOccurrenceId
E.&&. examOccScheduleOpt E.^. ExamOccurrenceScheduleOptUser E.==. user E.^. UserId
return $ examOccScheduleOpt E.^. ExamOccurrenceScheduleOptOpt
mCourseOpt = E.subSelect $ getCourseScheduleOpt course user
in E.where_ $ user E.^. UserId E.==. E.val uid
E.&&. E.fromMaybe
( ( E.fromMaybe
(user E.^. UserScheduleOccurrenceDisplayDefault)
mCourseOpt
) E.&&. ( isCourseLecturer muid ata (course E.^. CourseId)
E.||. ( mayViewCourse muid ata now course Nothing -- do NOT remove, this is actually necessary here!
-- (There can be exam participants that are
-- not enrolled, me thinks)
E.&&. E.maybe E.false (\visibleFrom -> visibleFrom E.<=. E.val now) (exam E.^. ExamVisibleFrom) -- is the exam visible?
E.&&. E.maybe E.false (\publishOcc -> publishOcc E.<=. E.val now) (exam E.^. ExamPublishOccurrenceAssignments) -- are the exam occurrence assignments visible?
E.&&. (E.exists $ E.from $ \examRegistration -> E.where_ $
examRegistration E.^. ExamRegistrationExam E.==. exam E.^. ExamId
E.&&. E.just (examRegistration E.^. ExamRegistrationUser) E.==. E.val muid
E.&&. E.maybe E.true (\registrationOccurrence -> E.maybe E.false (const E.true) mExamOccOpt E.||. registrationOccurrence E.==. examOcc E.^. ExamOccurrenceId) (examRegistration E.^. ExamRegistrationOccurrence) -- if registered for a specific occurrence, get only this one and occurrences with an opt-in, otherwise get every occurrence available
)
)
)
)
mExamOccOpt
examOccurrenceShouldBeDisplayedInSchedule _ _ _ _ _ _ = E.false
-- Local helper functions
getCourseScheduleOpt :: E.SqlExpr (Entity Course) -> E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (E.Value Bool))
getCourseScheduleOpt course user = E.from $ \courseScheduleOpt -> do
E.where_ $ courseScheduleOpt E.^. CourseScheduleOptCourse E.==. course E.^. CourseId
E.&&. courseScheduleOpt E.^. CourseScheduleOptUser E.==. user E.^. UserId
return $ courseScheduleOpt E.^. CourseScheduleOptOpt

View File

@ -0,0 +1,37 @@
module Utils.Schedule.Types
( module Utils.Schedule.Types
) where
import Import
import qualified Database.Esqueleto as E
import Utils.Schedule.Types.ScheduleEntry as Utils.Schedule.Types
import Utils.Schedule.Types.ScheduleView as Utils.Schedule.Types
import Utils.Schedule.Types.ScheduleOffset as Utils.Schedule.Types
import Utils.Schedule.Types.ScheduleOptions as Utils.Schedule.Types
-- TODO: replace Info types with one joined type and fetch info in one single runDB
type ScheduleCourseEventInfo = ( Entity Course
, Entity CourseEvent
, E.Value Bool -- ^ showRoom
)
type ScheduleTutorialInfo = ( Entity Course
, Entity Tutorial
, E.Value Bool -- ^ showRoom
)
type ScheduleExamOccurrenceInfo = ( Entity Course
, Entity Exam
, Entity ExamOccurrence
, E.Value Bool -- ^ showRoom
)
type ScheduleExamOccurrenceJoinedInfo = ( Entity Course
, Entity Exam
, NonEmpty ( Entity ExamOccurrence
, E.Value Bool -- ^ showRoom
)
)

View File

@ -0,0 +1,34 @@
module Utils.Schedule.Types.ScheduleEntry
( ScheduleEntry(..)
) where
import Import
data ScheduleEntry = ScheduleCourseEvent
{ sceCourse :: Entity Course
, sceType :: CourseEventType
, sceRoom :: Maybe RoomReference
, sceShowRoom :: Bool
, sceOccurrence :: Either OccurrenceException OccurrenceSchedule
, sceNoOccur :: Set LocalTime
, sceTerm :: Entity Term
}
| ScheduleTutorial
{ stCourse :: Entity Course
, stName :: TutorialName
, stType :: TutorialType
, stRoom :: Maybe RoomReference
, stShowRoom :: Bool
, stOccurrence :: Either OccurrenceException OccurrenceSchedule
, stNoOccur :: Set LocalTime
, stTerm :: Entity Term
}
| ScheduleExamOccurrence
{ seoCourse :: Entity Course
, seoExamName :: ExamName
, seoRooms :: Set (Maybe RoomReference, Bool)
, seoStart :: UTCTime
, seoEnd :: Maybe UTCTime
}
deriving (Generic, Typeable)

View File

@ -0,0 +1,25 @@
module Utils.Schedule.Types.ScheduleOffset
( ScheduleOffset(..)
, addOffset, offsetInDays
) where
import Import.NoModel
data ScheduleOffset = ScheduleOffsetNone
| ScheduleOffsetDays Int
deriving (Eq, Ord, Show, Read, Generic, Typeable)
derivePathPiece ''ScheduleOffset (camelToPathPiece' 1) "_"
-- | Join two ScheduleOffsets by addition
addOffset :: ScheduleOffset -> ScheduleOffset -> ScheduleOffset
addOffset ScheduleOffsetNone offset = offset
addOffset offset ScheduleOffsetNone = offset
addOffset (ScheduleOffsetDays d) (ScheduleOffsetDays d') = ScheduleOffsetDays $ d + d'
-- | Calculate number of offset days from ScheduleOffset
offsetInDays :: ScheduleOffset -> Int
offsetInDays ScheduleOffsetNone = 0
offsetInDays (ScheduleOffsetDays d) = d

View File

@ -0,0 +1,45 @@
module Utils.Schedule.Types.ScheduleOptions
( ScheduleOffset(..)
, ScheduleOptionsAction(..)
, ScheduleOptions(..)
) where
import Import
import Utils.Form
import Utils.Schedule.Types.ScheduleOffset
import Utils.Schedule.Types.ScheduleView
data ScheduleOptionsAction = ScheduleSetView
| ScheduleSetOffset ScheduleOffset
| ScheduleSetDefault
deriving (Eq, Ord, Show, Read, Generic, Typeable)
derivePathPiece ''ScheduleOptionsAction (camelToPathPiece' 1) "--"
data ScheduleOptions = ScheduleOptions
{ scheduleView :: ScheduleView
, scheduleOffset :: ScheduleOffset
, scheduleOptionsAction :: ScheduleOptionsAction
}
deriving (Eq, Ord, Show, Read, Generic, Typeable)
derivePathPiece ''ScheduleOptions (camelToPathPiece' 1) "---"
instance Button UniWorX ScheduleOptions where
btnClasses = const [BCIsButton]
btnLabel ScheduleOptions{..} = case scheduleOptionsAction of
ScheduleSetDefault -> i18n MsgScheduleReset
ScheduleSetView -> case scheduleView of
ScheduleViewWeek -> i18n MsgScheduleViewWeek
ScheduleSetOffset o -> case scheduleView of
ScheduleViewWeek -> let iconTooltipMessage i m = iconTooltip (i18n m) (Just i) True
o' = offsetInDays o
in if | o' <= (-7) -> iconTooltipMessage IconFastBackward MsgScheduleOffsetWeekBackwardWeek
| o' < 0 -> iconTooltipMessage IconBackward . MsgScheduleOffsetWeekBackwardDays $ abs o'
| o' == 0 -> iconTooltipMessage IconCurrent MsgScheduleOffsetWeekCurrent
| o' < 7 -> iconTooltipMessage IconForward . MsgScheduleOffsetWeekForwardDays $ abs o'
| otherwise -> iconTooltipMessage IconFastForward MsgScheduleOffsetWeekForwardWeek

View File

@ -0,0 +1,23 @@
module Utils.Schedule.Types.ScheduleView
( ScheduleView(..)
) where
import Import.NoModel
import Model.Types.TH.PathPiece
-- TODO: implement ScheduleViewDay and ScheduleViewMonth
data ScheduleView = ScheduleViewWeek
deriving (Eq, Ord, Enum, Show, Read, Generic, Typeable, NFData)
instance Bounded ScheduleView where
minBound = maxBound
maxBound = ScheduleViewWeek
instance Finite ScheduleView
instance Universe ScheduleView
nullaryPathPiece ''ScheduleView $ camelToPathPiece' 2
pathPieceJSON ''ScheduleView
pathPieceJSONKey ''ScheduleView
derivePersistFieldPathPiece ''ScheduleView

261
src/Utils/Schedule/Week.hs Normal file
View File

@ -0,0 +1,261 @@
module Utils.Schedule.Week
( weekOffsets, weekSchedule
) where
import Import
import qualified Data.CaseInsensitive as CI
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Database.Esqueleto as E
import Handler.Utils.DateTime (formatTimeRangeW, formatTimeW, utcToLocalTime, localTimeToUTCSimple)
import Handler.Utils.Term (fetchActiveTerms)
import Handler.Utils.Widgets (roomReferenceWidget)
import Utils.Schedule
import Utils.Schedule.Types
import Utils.Schedule.Week.SlotAssociation
import Utils.Schedule.Week.TimeSlot
weekOffsets :: UTCTime -> Entity User -> ScheduleOffset -> [ScheduleOffset]
weekOffsets now user@(Entity _ User{userScheduleWeekDays = ScheduleWeekDays userScheduleWeekDays}) scheduleOffset
= nub [ ScheduleOffsetDays (-7)
, ScheduleOffsetDays $ go (-1)
, ScheduleOffsetNone
, ScheduleOffsetDays $ go 1
, ScheduleOffsetDays 7
]
where
go d
| weeksEqual 0 d
, abs d < 7
= go d'
| d >= 0
, abs d < 7
, weeksEqual d d'
= go d'
| otherwise
= d
where d' = bool pred succ (d >= 0) d
weeksEqual = on (==) $ filter (\d' -> dayOfWeek d' `elem` userScheduleWeekDays) . week
where week d = weekDays now user $ ScheduleOffsetDays d `addOffset` scheduleOffset
weekSchedule :: UTCTime -> Entity User -> ScheduleOffset -> Widget
weekSchedule now user@(Entity uid User{userScheduleWeekDays = ScheduleWeekDays userScheduleWeekDays, ..}) scheduleOffset = do
ata <- getSessionActiveAuthTags
let localNow = utcToLocalTime now
let
dayNowOffset = toInteger (offsetInDays scheduleOffset) `addDays` localDay (utcToLocalTime now)
week = weekDays now user scheduleOffset
lectureDay (Entity _ Term{..}) d = termLectureStart <= d && d <= termLectureEnd
&& d `notElem` termHolidays
isToday d = d == localDay localNow
isCurrentSlot = isInTimeSlot $ localTimeOfDay localNow
isCurrentScheduleEntry d ts = \case
ScheduleCourseEvent{sceOccurrence,sceNoOccur} -> not (localNow `Set.member` sceNoOccur) && case sceOccurrence of
Left ExceptNoOccur{..} -> localNow /= exceptTime -- TODO: sceNoOccur and Left ExceptNoOccur{..} seem to be the same case
Left ExceptOccur{..} -> d == exceptDay && timeOfDayToUTC exceptStart <= now && now < timeOfDayToUTC exceptEnd
Right ScheduleWeekly{..} -> scheduleDayOfWeek == dayOfWeek d && timeOfDayToUTC scheduleStart <= now && now < timeOfDayToUTC scheduleEnd
ScheduleTutorial{stOccurrence,stNoOccur} -> not (localNow `Set.member` stNoOccur) && case stOccurrence of
Left ExceptNoOccur{..} -> localNow /= exceptTime -- TODO: sceNoOccur and Left ExceptNoOccur{..} seem to be the same case
Left ExceptOccur{..} -> d == exceptDay && timeOfDayToUTC exceptStart <= now && now < timeOfDayToUTC exceptEnd
Right ScheduleWeekly{..} -> scheduleDayOfWeek == dayOfWeek d && timeOfDayToUTC scheduleStart <= now && now < timeOfDayToUTC scheduleEnd
ScheduleExamOccurrence{seoStart,seoEnd} -> seoStart <= now && now < (fromMaybe (view _2 $ timeSlotToUTCTime d ts) seoEnd)
where
timeOfDayToUTC = localTimeToUTCSimple . LocalTime d
(activeTerms, courseEvents, tutorials, examOccurrences) <- liftHandler . runDB $ (,,,)
<$> fetchActiveTerms
-- TODO: fetch course events for this week only?
<*> fetchCourseEventsScheduleInfo (Just uid) ata now
<*> fetchTutorialsScheduleInfo (Just uid) ata now
-- TODO: this makes the exam table partly redundant => maybe remove?
<*> fetchExamOccurrencesScheduleInfo (Just uid) ata now
let
holidays = concatMap (termHolidays . entityVal) activeTerms
courseEventToScheduleEntries :: ScheduleCourseEventInfo -> [ScheduleEntry]
courseEventToScheduleEntries (sceCourse@(Entity _ Course{..}), Entity _ CourseEvent{courseEventType=sceType,courseEventRoom=sceRoom,courseEventTime=Occurrences{..}}, E.Value sceShowRoom)
| [sceTerm] <- filter ((== courseTerm) . entityKey) activeTerms
, termActive $ entityVal sceTerm
= let scheduleds
= Set.toList occurrencesScheduled <&> \scheduled ->
let sceOccurrence = Right scheduled in ScheduleCourseEvent{..}
exceptions
= Set.toList occurrencesExceptions <&> \exception ->
let sceOccurrence = Left exception in ScheduleCourseEvent{..}
sceNoOccur = setOf (folded . _ExceptNoOccur) occurrencesExceptions
in scheduleds <> exceptions
| otherwise = mempty
tutorialToScheduleEntries :: ScheduleTutorialInfo -> [ScheduleEntry]
tutorialToScheduleEntries (stCourse@(Entity _ Course{..}), Entity _ Tutorial{tutorialName=stName,tutorialType=stType,tutorialRoom=stRoom,tutorialTime=Occurrences{..}}, E.Value stShowRoom)
| [stTerm] <- filter ((== courseTerm) . entityKey) activeTerms
, termActive $ entityVal stTerm
= let scheduleds
= Set.toList occurrencesScheduled <&> \scheduled ->
let stOccurrence = Right scheduled in ScheduleTutorial{..}
exceptions
= Set.toList occurrencesExceptions <&> \exception ->
let stOccurrence = Left exception in ScheduleTutorial{..}
stNoOccur = setOf (folded . _ExceptNoOccur) occurrencesExceptions
in scheduleds <> exceptions
| otherwise
= mempty
joinParallelExamOccurrences :: [ScheduleExamOccurrenceInfo] -> [ScheduleExamOccurrenceJoinedInfo]
joinParallelExamOccurrences = go [] where
go acc [] = acc
go acc (examOcc@(course, exam, occ, showRoom):examOccs) =
let ((((\(_,_,o,s) -> (o,s)) <$>) -> parallel), other) = partition (examOcc `isParallelTo`) examOccs
in go ((course, exam, (occ,showRoom):|parallel):acc) other
(Entity cid _, Entity eid _, Entity _ occ, _) `isParallelTo` (Entity cid' _, Entity eid' _, Entity _ occ', _) =
cid == cid' && eid == eid'
&& examOccurrenceStart occ == examOccurrenceStart occ' && examOccurrenceEnd occ == examOccurrenceEnd occ'
examOccurrenceToScheduleEntry :: ScheduleExamOccurrenceJoinedInfo -> ScheduleEntry
examOccurrenceToScheduleEntry (seoCourse@(Entity _ Course{}), Entity _ Exam{examName=seoExamName}, examOccs@((Entity _ occ, _):|_)) =
let seoRooms = (Set.fromList . toList) $ (\(Entity _ ExamOccurrence{examOccurrenceRoom}, E.Value showRoom) -> (examOccurrenceRoom, showRoom)) <$> examOccs
seoStart = examOccurrenceStart occ -- multiple exam occurrences are joined on equality of start and end,
seoEnd = examOccurrenceEnd occ -- so taking the timestamps of the first occurrence suffices
in ScheduleExamOccurrence{..}
events'' :: Map Day (Map TimeSlot [(ScheduleEntry, SlotAssociation)])
events'' = Map.fromList $ week <&> \d ->
( d
, Map.fromList $ allTimeSlots <&> \slot ->
( slot
, mapMaybe (\entry -> (entry, ) <$> seIsInSlot d slot entry) scheduleEntries
)
) where
scheduleEntries = join $ (courseEventToScheduleEntries <$> courseEvents)
<> (tutorialToScheduleEntries <$> tutorials)
<> pure (examOccurrenceToScheduleEntry <$> (joinParallelExamOccurrences examOccurrences))
events' :: Map Day (Map TimeSlot [(ScheduleEntry, SlotAssociation)])
events' = flip imap events'' $ \currentDay slotsPerDay -> slotsPerDay <&> \occurrencesInSlot ->
let
isRegularWithoutException :: ScheduleEntry -> Bool
isRegularWithoutException =
let -- remove regular occurrences if there is a NoOccur exception for the occurrence of this week
goPrune noOccurs term = \case
Right ScheduleWeekly{..} -> and
[ lectureDay term currentDay
, flip none noOccurs $
\needle -> let localDay = scheduleDayOfWeek `dayOfWeekToDayWith` dayNowOffset
in LocalTime{ localDay, localTimeOfDay = scheduleStart } <= needle
&& needle <= LocalTime{ localDay, localTimeOfDay = scheduleEnd }
]
Left ExceptOccur{} -> True
-- remove NoOccur exceptions
Left ExceptNoOccur{} -> False
in \case
ScheduleCourseEvent{..} -> goPrune sceNoOccur sceTerm sceOccurrence
ScheduleTutorial{..} -> goPrune stNoOccur stTerm stOccurrence
_ -> True
in sortOn (views _1 $ scheduleEntryStartUTC currentDay) $ filter (views _1 isRegularWithoutException) occurrencesInSlot
-- TODO: perform this filtering asap, in DB fetch if possible
events :: Map Day (Map TimeSlot [(ScheduleEntry, SlotAssociation)])
events = Map.filterWithKey shouldBeDisplayedOrHasEvents events' where
shouldBeDisplayedOrHasEvents d entries = dayOfWeek d `elem` userScheduleWeekDays || any (not . null) entries
timeSlotsDefaultDisplay :: Set TimeSlot
timeSlotsDefaultDisplay = Set.fromList $ timeSlotsFromTo userScheduleWeekTimeslotLength userScheduleWeekTimeFrom userScheduleWeekTimeTo
allTimeSlots :: [TimeSlot]
allTimeSlots = timeSlotsAll userScheduleWeekTimeslotLength userScheduleWeekTimeFrom
timeSlotIsEmpty :: TimeSlot -> Bool
timeSlotIsEmpty slot = foldr (\d acc -> acc && maybe True null (d Map.!? slot)) True events
$(widgetFile "schedule/week")
-- Local helper functions
-- | Get days that are to be displayed in the week schedule
weekDays :: UTCTime -> Entity User -> ScheduleOffset -> [Day]
weekDays now (Entity _ User{userWeekStart}) scheduleOffset = go dayNowOffset
where go d
| dayOfWeek d == firstDay = [d .. addDays 6 d]
| otherwise = go $ pred d
firstDay = toEnum $ fromEnum userWeekStart + offsetInDays scheduleOffset
dayNowOffset = toInteger (offsetInDays scheduleOffset) `addDays` localDay (utcToLocalTime now)
-- | Check whether a given ScheduleEntry lies in a given TimeSlot
seIsInSlot :: Day -> TimeSlot -> ScheduleEntry -> Maybe SlotAssociation
seIsInSlot d slot = \case
ScheduleCourseEvent{sceOccurrence} -> occurrenceIsInSlot sceOccurrence
ScheduleTutorial{stOccurrence} -> occurrenceIsInSlot stOccurrence
ScheduleExamOccurrence{seoStart, seoEnd = Nothing}
-> let associated = slotBegin <= seoStart && seoStart < slotEnd
in guardOn associated SlotBegins
ScheduleExamOccurrence{seoStart, seoEnd = Just seoEnd}
-> let associated = seoEnd > slotBegin && seoStart < slotEnd
in guardOn associated $ _SlotAssociation # ( slotBegin <= seoStart && seoStart < slotEnd
, slotBegin <= seoEnd && seoEnd <= slotEnd
)
where
(slotBegin, slotEnd) = timeSlotToUTCTime d slot
occurrenceIsInSlot occurrence = guardOn associated $ _SlotAssociation # ( slotBegin <= occStart && occStart < slotEnd
, slotBegin <= occEnd && occEnd <= slotEnd
)
where
associated = occEnd > slotBegin && occStart < slotEnd
occStart = localTimeToUTCSimple $ LocalTime occDay occStartTime
occEnd = localTimeToUTCSimple $ LocalTime occDay occEndTime
(occDay, occStartTime, occEndTime) = case occurrence of
Right ScheduleWeekly{..} -> (scheduleDayOfWeek `dayOfWeekToDayWith` d, scheduleStart, scheduleEnd)
Left ExceptOccur{..} -> (exceptDay, exceptStart, exceptEnd)
Left ExceptNoOccur{exceptTime=LocalTime{..}} -> (localDay, localTimeOfDay, localTimeOfDay)
-- | To which route should each schedule entry link to?
scheduleEntryToHref :: ScheduleEntry -> Route UniWorX
scheduleEntryToHref = \case
ScheduleCourseEvent{sceCourse=(Entity _ Course{..})} -> CourseR courseTerm courseSchool courseShorthand CShowR -- TODO: link to events table? (currently has no id)
ScheduleTutorial{stCourse=(Entity _ Course{..})} -> CourseR courseTerm courseSchool courseShorthand CShowR -- TODO: link to table with id "tutorials"?
ScheduleExamOccurrence{seoCourse=(Entity _ Course{..}),seoExamName} -> CExamR courseTerm courseSchool courseShorthand seoExamName EShowR
-- | At which UTCTime does a ScheduleEntry start, given a specific day?
scheduleEntryStartUTC :: Day -> ScheduleEntry -> UTCTime
scheduleEntryStartUTC currentDay = \case
ScheduleCourseEvent{sceOccurrence} -> occurrenceToStart sceOccurrence
ScheduleTutorial{stOccurrence} -> occurrenceToStart stOccurrence
ScheduleExamOccurrence{seoStart} -> seoStart
where
occurrenceToStart = \case
Left ExceptOccur{exceptDay, exceptStart} -> localTimeToUTCSimple $ LocalTime exceptDay exceptStart
Left ExceptNoOccur{exceptTime} -> localTimeToUTCSimple exceptTime
Right ScheduleWeekly{scheduleStart} -> localTimeToUTCSimple $ LocalTime currentDay scheduleStart
-- | Calls formatTimeRangeW with the correct arguments and prepends an occurrence descriptor based on the occurrence type
formatEitherOccurrenceW :: Either OccurrenceException OccurrenceSchedule -> Widget
formatEitherOccurrenceW = \case
Right ScheduleWeekly{..} -> [whamlet| _{MsgScheduleTime}: |] <> formatTimeRangeW SelFormatTime scheduleStart (Just scheduleEnd)
Left ExceptOccur{..} -> [whamlet| _{MsgScheduleOccur}: |] <> formatTimeRangeW SelFormatTime (LocalTime exceptDay exceptStart) (Just $ LocalTime exceptDay exceptEnd)
Left ExceptNoOccur{} -> [whamlet| _{MsgScheduleNoOccur} |] -- <> formatTimeW SelFormatDateTime exceptTime
-- | Uniquely identify each day as table head
-- | This avoids constantly hiding e.g. some DayOfWeek (which would interfere with day offsets)
dayTableHeadIdent :: Day -> Text
dayTableHeadIdent = tshow . toModifiedJulianDay
-- | Convert from DayOfWeek to Day of this week using the current day
dayOfWeekToDayWith :: DayOfWeek -> Day -> Day
dayOfWeekToDayWith weekDay = go where
go d | weekDay' == weekDay = d
| weekDay' > weekDay = go $ pred d
| otherwise = go $ succ d
where weekDay' = dayOfWeek d
-- | Auxiliary definition to be used in templates since ranges are not parsed correctly
indexedList :: [a] -> [(Int, a)]
indexedList = zip [0..]

View File

@ -0,0 +1,34 @@
module Utils.Schedule.Week.SlotAssociation
( SlotAssociation(..)
, _SlotAssociation
, slotAssocIsCont
) where
import Import
data SlotAssociation
= SlotIntersects -- ^ Slot is true subset of event
| SlotEnds -- ^ Event ends in slot, but does not begin within
| SlotBegins -- ^ Event begins in slot, but does not end within
| SlotContained -- ^ Event starts and ends within slot
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)
nullaryPathPiece ''SlotAssociation $ camelToPathPiece' 1
_SlotAssociation :: Iso' SlotAssociation (Bool, Bool)
_SlotAssociation = iso toBools fromBools
where
toBools = \case
SlotIntersects -> (False, False)
SlotEnds -> (False, True )
SlotBegins -> (True, False)
SlotContained -> (True, True )
fromBools = \case
(False, False) -> SlotIntersects
(False, True ) -> SlotEnds
(True, False) -> SlotBegins
(True, True ) -> SlotContained
slotAssocIsCont :: SlotAssociation -> Bool
slotAssocIsCont = views (_SlotAssociation . _1) not

View File

@ -0,0 +1,67 @@
module Utils.Schedule.Week.TimeSlot
( TimeSlot(..)
, timeSlots, timeSlotsFromTo, timeSlotsAll
, isInTimeSlot
, nextTimeSlot
, timeSlotToUTCTime
, formatTimeSlotW
) where
import Import
import Handler.Utils.DateTime (formatTimeRangeW, localTimeToUTCSimple)
import Utils.Schedule.Week.Types.TimeSlot
-- TODO: This module needs major refactoring
timeSlots :: Bool -- ^ Only slots between from/to?
-> NominalDiffTime -- ^ Step
-> NominalDiffTime -- ^ From
-> NominalDiffTime -- ^ To
-> [TimeSlot]
timeSlots onlyFromTo (abs -> slotStep) f t
| t < f = timeSlots onlyFromTo slotStep t f
| slotStep <= 0 = error "Invalid slotStep"
| otherwise = reverse [ TimeSlot{..}
| tsTo <- [f,f - slotStep..0]
, let tsFrom = tsTo - slotStep
, not onlyFromTo || tsFrom >= f
, tsFrom >= 0
]
++ [ TimeSlot{..}
| tsFrom <- [f,f + slotStep..nominalDay]
, let tsTo = tsFrom + slotStep
, not onlyFromTo || tsTo <= t
, tsTo <= nominalDay
]
timeSlotsFromTo :: NominalDiffTime -- ^ Step
-> NominalDiffTime -- ^ From
-> NominalDiffTime -- ^ To
-> [TimeSlot]
timeSlotsFromTo = timeSlots True
timeSlotsAll :: NominalDiffTime -- ^ Step
-> NominalDiffTime -- ^ From
-> [TimeSlot]
timeSlotsAll step f = timeSlots False step f f -- @t@ is unused in `timeSlots`, iff @onlyFromTo@ is `False`
-- | Check whether a given time of day lies within a given TimeSlot
isInTimeSlot :: TimeOfDay -> TimeSlot -> Bool
isInTimeSlot (timeOfDayToNominalTime -> time) TimeSlot{..} = tsFrom <= time && time < tsTo
-- | Get the successor of a TimeSlot
nextTimeSlot :: NominalDiffTime -> TimeSlot -> TimeSlot
nextTimeSlot slotStep TimeSlot{..} = TimeSlot{ tsFrom = tsTo, tsTo = tsTo + slotStep }
-- | Convert a TimeSlot to UTCTime for a given TimeZone
timeSlotToUTCTime :: Day -> TimeSlot -> (UTCTime, UTCTime)
timeSlotToUTCTime d TimeSlot{..} = (nominalDiffTimeToUTC tsFrom, nominalDiffTimeToUTC tsTo)
where nominalDiffTimeToUTC = localTimeToUTCSimple . LocalTime d . nominalTimeToTimeOfDay
-- | Format a given TimeSlot as time range
formatTimeSlotW :: TimeSlot -> Widget
formatTimeSlotW TimeSlot{..} = formatTimeRangeW SelFormatTime (nominalTimeToTimeOfDay tsFrom) $ Just (nominalTimeToTimeOfDay tsTo)

View File

@ -0,0 +1,5 @@
module Utils.Schedule.Week.Types
( module Utils.Schedule.Week.Types
) where
import Utils.Schedule.Week.Types.TimeSlot as Utils.Schedule.Week.Types

View File

@ -0,0 +1,14 @@
module Utils.Schedule.Week.Types.TimeSlot
( TimeSlot(..)
) where
import Import.NoModel
-- | Half-open interval of time
--
-- Fields are to be interpreted as time since midnight
data TimeSlot = TimeSlot
{ tsFrom :: NominalDiffTime -- ^ Inclusive
, tsTo :: NominalDiffTime -- ^ Exclusive
} deriving (Eq, Ord, Show, Generic, Typeable)

25
src/Utils/Tutorial.hs Normal file
View File

@ -0,0 +1,25 @@
module Utils.Tutorial
( isTutorialTutor, isTutorialParticipant
) where
import Import
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
isTutorialTutor :: Maybe UserId -> AuthTagActive -> E.SqlExpr (E.Value TutorialId) -> E.SqlExpr (E.Value Bool)
isTutorialTutor muid AuthTagActive{..} tid
| Just uid <- muid, authTagIsActive AuthTutor = E.exists . E.from $ \(tutor `E.InnerJoin` tutorial) -> do
E.on $ tutor E.^. TutorTutorial E.==. tutorial E.^. TutorialId
E.where_ $ tutorial E.^. TutorialId E.==. tid
E.&&. tutor E.^. TutorUser E.==. E.val uid
| otherwise = E.false
isTutorialParticipant :: Maybe UserId -> AuthTagActive -> E.SqlExpr (E.Value TutorialId) -> E.SqlExpr (E.Value Bool)
isTutorialParticipant muid AuthTagActive{..} tid
| Just uid <- muid, authTagIsActive AuthTutorialRegistered = E.exists . E.from $ \(tutorialParticipant `E.InnerJoin` tutorial) -> do
E.on $ tutorialParticipant E.^. TutorialParticipantTutorial E.==. tutorial E.^. TutorialId
E.where_ $ tutorial E.^. TutorialId E.==. tid
E.&&. tutorialParticipant E.^. TutorialParticipantUser E.==. E.val uid
| otherwise = E.false

View File

@ -243,12 +243,16 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
_{MsgCourseEventRoom}
<th .table__th uw-hide-column-header="note" :hiddenEventNotes:.course-event-note--hidden>
_{MsgCourseEventNote}
$# TODO: merge with actions column
$if is _Just mbAuth
<th .table__th uw-hide-column-header="schedule-actions">
_{MsgScheduleOptActions}
$if mayCreateEvents
<th .table__th uw-hide-column-header="actions">
_{MsgCourseEventActions}
\ #{iconInvisible}
<tbody>
$forall (cID, CourseEvent{courseEventType, courseEventTime, courseEventRoom, courseEventNote}, showRoom) <- events
$forall (cID, CourseEvent{courseEventType, courseEventTime, courseEventRoom, courseEventNote}, showRoom, courseEventCurrentOpt, mEventScheduleOpt) <- events
<tr .table__row ##{"event-" <> toPathPiece cID}>
<td .table__td>
<div .table__td-content>
@ -269,6 +273,15 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
<td .table__td :hiddenEventNotes:.course-event-note--hidden>
<div .table__td-content>
#{courseEventNote}
$# TODO: merge with actions column
$if is _Just mbAuth
<td .table__td>
<div .table__td-content>
<a .btn .btn-primary href=@{CEventR tid ssh csh cID (CEvScheduleOptSetR (not courseEventCurrentOpt))}>
_{bool MsgScheduleOptIn MsgScheduleOptOut courseEventCurrentOpt}
$if is _Just mEventScheduleOpt
<a .btn .btn-primary href=@{CEventR tid ssh csh cID CEvScheduleOptDelR}>
_{MsgScheduleOptDelete}
$if mayCreateEvents
<td .table__td>
<ul .list--inline .list--iconless .list--comma-separated>
@ -293,3 +306,12 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
<dd .deflist__dd>
^{tutorialTable}
$maybe (_, user) <- mbAuth
<dt .deflist__dt>
_{MsgScheduleOptActions}
<dd .deflist__dd>
<a .btn .btn-primary href=@{CourseR tid ssh csh (CScheduleOptSetR (not (courseScheduleOptToggleValue user)))}>
_{bool MsgCourseScheduleOptIn MsgCourseScheduleOptOut (courseScheduleOptToggleValue user)}
$if is _Just mCourseScheduleOpt
<a .btn .btn-primary href=@{CourseR tid ssh csh CScheduleOptDelR}>
_{MsgCourseScheduleOptDelete}

View File

@ -224,7 +224,7 @@ $if not (null occurrences)
$if not occurrenceAssignmentsVisible
\ ^{isVisible False}
$of _
<th .table__td>
<th .table__th>
$if not occurrenceAssignmentsVisible
^{isVisible False}
$if showRegisteredCount
@ -232,8 +232,11 @@ $if not (null occurrences)
_{MsgExamRegisteredCount}
\ ^{isVisible False}
<th .table__th>_{MsgExamRoomDescription}
$if is _Just mAuth
<th .table__th>
_{MsgSchedule}
<tbody>
$forall (occurrence, registered, rCount, showRoom) <- occurrences
$forall (occurrence, registered, rCount, showRoom, shouldBeDisplayedInSchedule, mEOScheduleOpt) <- occurrences
$with Entity _occId ExamOccurrence{examOccurrenceName, examOccurrenceRoom, examOccurrenceStart, examOccurrenceEnd, examOccurrenceDescription} <- occurrence
$with registerWdgt <- registerWidget (Just occurrence)
<tr .table__row :markUnregisteredOccurrences (Just occurrence) && not registered:.occurrence--not-registered>
@ -268,6 +271,14 @@ $if not (null occurrences)
<td .table__td>
$maybe desc <- examOccurrenceDescription
#{desc}
$if is _Just mAuth
<td .table__td>
<div .table__td-content>
<a .btn .btn-primary href=@{CExamR tid ssh csh examn (EScheduleOptSetR examOccurrenceName (not shouldBeDisplayedInSchedule))}>
_{bool MsgScheduleOptIn MsgScheduleOptOut shouldBeDisplayedInSchedule}
$if is _Just mEOScheduleOpt
<a .btn .btn-primary href=@{CExamR tid ssh csh examn (EScheduleOptDelR examOccurrenceName)}>
_{MsgScheduleOptDelete}
<tfoot>
<tr .table__row .table__row--sum>
$if occurrenceNamesShown

View File

@ -0,0 +1,17 @@
$# TODO: Erklärungen (bzw. Teile davon) in FAQ wiederverwenden
<p>
In Ihrer persönlichen Terminübersicht werden Ihnen Ihre Termine zu Uni2work-Kursen angezeigt.
<p>
Sie können auf einzelne Termine in Ihrer Terminübersicht klicken, um zum jeweiligen Kurs, zum jeweiligen Tutorium oder zur jeweiligen Prüfung zu gelangen.
<p>
Es gibt die Möglichkeit, einzelne Termine aus Ihrer Terminübersicht auszublenden. Wenn Sie einzelne Termine aus Ihrer persönlichen Terminübersicht ausblenden möchten, dann klicken Sie zunächst auf den jeweils auszublendenden Termin, und fügen dann über den Knopf "Aus Terminübersicht ausblenden" eine Ausnahme für diesen Termin hinzu. <br />
Analog dazu können Sie ausgeblendete Termine auch in Ihrer Terminübersicht einblenden, indem Sie den Knopf "In Terminübersicht zeigen" betätigen; diesen finden Sie an der gleichen Stelle, an der sonst der Knopf "Aus Terminübersicht ausblenden" zu finden ist. Alternativ können Sie auch über Ihre Benutzereinstellungen Ihre Terminausnahmen einsehen und entfernen. <br />
In Ihren Benutzereinstellungen können Sie festlegen, ob Termine von Kursen, zu denen Sie sich neu anmelden standardmäßig in Ihrer Terminübersicht angezeigt werden sollen.
<p>
In Ihren Benutzereinstellungen haben Sie verschiedene Möglichkeiten, die Anzeige Ihrer Terminübersicht zu beeinflussen. <br />
Beispielsweise können Sie dort in der Wochenübersicht darzustellende Wochentage festlegen (Samstag und Sonntag sind standrdmäßig ausgeblendet, wenn Sie an diesem Tag keinen Termin haben), oder auch Beginn und Ende (Uhrzeit) eines Tages in der Wochenübersicht sowie die Länge der darzustellenden Zeitslots (d.h. einer Zeilen) festlegen. Sie können dort auch festlegen, ob Termine von Kursen, zu denen Sie sich neu anmelden standardmäßig in Ihrem Stundenplan angezeigt werden sollen.
Die relevanten Einstellungen hierzu finden Sie in Ihren Benutzereinstellungen (diese erreichen Sie über "Anpassen" rechts oben), und dort in der Sektion "Terminübersicht".

View File

@ -0,0 +1,18 @@
$# TODO: Erklärungen (bzw. Teile davon) in FAQ wiederverwenden
<p>
This is your personal schedule, in which your appointments for your Uni2work courses are displayed.
<p>
You can click on a specific appointment to reach the respective course, tutorial or exam.
<p>
You may hide specific appointments from your personal schedule. To hide a specific appointment, please click on the appointment in your schedule, and hit the button "Hide from schedule" there to opt-out of this appointment in your schedule. <br />
Similarly, you may also show specific appointments in your schedule; in this case, hit the button "Show in schedule" (which will be displayed at the same place where you normally find the button "Hide from schedule"). Alternatively, you may also view and remove your appointment display opt-ins/opt-outs in your user settings. <br />
In your user settings, you may specify whether appointments for courses should be displayed by default after you registered for the course.
<p>
You may change the look and behaviour of your personal schedule in various ways in your user settings. <br />
Beispielsweise können Sie dort in der Wochenübersicht darzustellende Wochentage festlegen (Samstag und Sonntag sind standrdmäßig ausgeblendet, wenn Sie an diesem Tag keinen Termin haben), oder auch Beginn und Ende (Uhrzeit) eines Tages in der Wochenübersicht sowie die Länge der darzustellenden Zeitslots (d.h. einer Zeilen) festlegen. Sie können dort auch festlegen, ob Termine von Kursen, zu denen Sie sich neu anmelden standardmäßig in Ihrem Stundenplan angezeigt werden sollen.
For example, you may change the weekdays which should be displayed by default in your weekly schedule (Saturdays and Sundays are hidden by default if you do not have any appointments on this day), or you may change the begin and end (time) of each day in your weekly schedule such as the length of each time slot (i.e. the time length of each row). You may also specify whether appointments for new courses should be displayed in your schedule by default after you register for the course. <br />
All relevant settings can be found in your user settings (which you can view via "Settings" in the upper right corner), and there under the section "Schedule".

View File

@ -0,0 +1,6 @@
$newline never
<section>
<h2>
<a href=@{ScheduleR}>
_{MsgSchedule}
^{schedule}

10
templates/schedule.hamlet Normal file
View File

@ -0,0 +1,10 @@
$newline never
<section>
^{schedule}
<form enctype=#{optionsEnctype} .schedule-options>
^{optionsWidget}
<section .explanation>
^{scheduleExplanation}

View File

@ -0,0 +1,10 @@
$newline never
#{csrf}
$if length viewWidgets > 1
$forall vWgt <- viewWidgets
^{fvWidget vWgt}
$forall oWgt <- offsetWidgets
^{fvWidget oWgt}

View File

@ -0,0 +1,77 @@
$newline never
<div .schedule uw-hide-columns="schedule-week">
<table .table .table--striped .table--hover .schedule>
<thead>
<tr .table__row .table__row--head>
<th .table__th uw-hide-column-header="time">
_{MsgScheduleTableHeadTime}
$forall day <- week
$if is _Just (Map.lookup day events)
<th .table__th :isToday day:.schedule-current uw-hide-column-header=#{dayTableHeadIdent day}>
^{formatTimeW SelFormatDate day}
$if elem day holidays
\ (_{MsgScheduleWeekHoliday})
<tbody>
$forall slot <- allTimeSlots
$if Set.member slot timeSlotsDefaultDisplay || not (timeSlotIsEmpty slot)
<tr .table__row>
<th .table__th uw-hide-columns--no-hide :any isToday week && isCurrentSlot slot:.schedule-current>
^{formatTimeSlotW slot}
$forall day <- week
$maybe dayEvents <- Map.lookup day events
$maybe slotEvents <- Map.lookup slot dayEvents
<td .table__td>
<div .table__td-content>
$forall (scheduleEntry, slotAssociation) <- slotEvents
<a href=@{scheduleEntryToHref scheduleEntry} .schedule--entry-link>
<div .schedule--entry .schedule--entry__#{toPathPiece slotAssociation} :slotAssocIsCont slotAssociation:.schedule--entry__continuation :isCurrentScheduleEntry day slot scheduleEntry:.schedule-current>
$case scheduleEntry
$of ScheduleCourseEvent{sceCourse=Entity _ Course{courseName},sceType,sceRoom,sceShowRoom,sceOccurrence}
#{CI.original courseName}: #{CI.original sceType} #
$if slotAssocIsCont slotAssociation
(_{MsgScheduleWeekSlotIsCont})
<br>
$if sceShowRoom
$maybe room <- sceRoom
_{MsgScheduleRoom}: ^{roomReferenceWidget room}
<br>
^{formatEitherOccurrenceW sceOccurrence}
$of ScheduleTutorial{stCourse=Entity _ Course{courseName},stName,stType,stRoom,stShowRoom,stOccurrence}
#{CI.original courseName}: #{stName} #
(
#{CI.original stType}
$if slotAssocIsCont slotAssociation
, _{MsgScheduleWeekSlotIsCont}
)
<br>
$if stShowRoom
$maybe room <- stRoom
_{MsgScheduleRoom}: ^{roomReferenceWidget room}
<br>
^{formatEitherOccurrenceW stOccurrence}
$of ScheduleExamOccurrence{seoCourse=Entity _ Course{courseName},seoExamName,seoRooms,seoStart,seoEnd}
#{CI.original courseName}: #{seoExamName} #
$if slotAssocIsCont slotAssociation
(_{MsgScheduleWeekSlotIsCont})
<br>
$case Set.toList seoRooms
$of []
$of [(mRoom, showRoom)]
$if showRoom
$maybe room <- mRoom
_{MsgScheduleRoom}: ^{roomReferenceWidget room}
<br>
$of more
_{MsgScheduleRooms}: #
$forall (idx,(mRoom,showRoom)) <- indexedList more
$if showRoom
$maybe room <- mRoom
^{roomReferenceWidget room}
$if idx < pred (length more)
; #
<br>
_{MsgScheduleOccur}: #
$if Just (utctDay seoStart) == fmap utctDay seoEnd
^{formatTimeRangeW SelFormatTime seoStart seoEnd}
$else
^{formatTimeRangeW SelFormatDateTime seoStart seoEnd}

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Sarah Vaupel <vaupel.sarah@campus.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
_{MsgExceptionKindNoOccur}: #{exceptTime'}

View File

@ -1,7 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Sarah Vaupel <vaupel.sarah@campus.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
_{MsgExceptionKindOccur}: #{exceptStart'}#{exceptEnd'}

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -98,6 +98,7 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Just $ Languages ["en"]
@ -109,6 +110,12 @@ fillDb = do
, userSex = Just SexMale
, userBirthday = Nothing
, userShowSex = userDefaultShowSex
, userScheduleView = userDefaultScheduleView
, userScheduleWeekDays = userDefaultScheduleWeekDays
, userScheduleWeekTimeFrom = userDefaultScheduleWeekTimeFrom
, userScheduleWeekTimeTo = userDefaultScheduleWeekTimeTo
, userScheduleWeekTimeslotLength = userDefaultScheduleWeekTimeslotLength
, userScheduleOccurrenceDisplayDefault = userDefaultScheduleOccurrenceDisplayDefault
, userTelephone = Nothing
, userMobile = Nothing
, userCompanyPersonalNumber = Just "00000"
@ -138,6 +145,7 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Nothing
@ -148,6 +156,12 @@ fillDb = do
, userCsvOptions = def { csvFormat = csvPreset # CsvPresetExcel }
, userSex = Just SexMale
, userShowSex = userDefaultShowSex
, userScheduleView = userDefaultScheduleView
, userScheduleWeekDays = userDefaultScheduleWeekDays
, userScheduleWeekTimeFrom = userDefaultScheduleWeekTimeFrom
, userScheduleWeekTimeTo = userDefaultScheduleWeekTimeTo
, userScheduleWeekTimeslotLength = userDefaultScheduleWeekTimeslotLength
, userScheduleOccurrenceDisplayDefault = userDefaultScheduleOccurrenceDisplayDefault
, userBirthday = Nothing
, userMobile = Nothing
, userTelephone = Nothing
@ -184,6 +198,7 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Nothing
@ -195,6 +210,12 @@ fillDb = do
, userBirthday = Just $ n_day $ 35 * (-365)
, userCsvOptions = def
, userShowSex = userDefaultShowSex
, userScheduleView = userDefaultScheduleView
, userScheduleWeekDays = userDefaultScheduleWeekDays
, userScheduleWeekTimeFrom = userDefaultScheduleWeekTimeFrom
, userScheduleWeekTimeTo = userDefaultScheduleWeekTimeTo
, userScheduleWeekTimeslotLength = userDefaultScheduleWeekTimeslotLength
, userScheduleOccurrenceDisplayDefault = userDefaultScheduleOccurrenceDisplayDefault
, userTelephone = Just "+49 69 690-71706"
, userMobile = Just "0173 69 99 646"
, userCompanyPersonalNumber = Just "57138"
@ -224,6 +245,7 @@ fillDb = do
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Just $ Languages ["de"]
@ -235,6 +257,12 @@ fillDb = do
, userSex = Just SexMale
, userBirthday = Just $ n_day $ 27 * (-365)
, userShowSex = userDefaultShowSex
, userScheduleView = userDefaultScheduleView
, userScheduleWeekDays = userDefaultScheduleWeekDays
, userScheduleWeekTimeFrom = userDefaultScheduleWeekTimeFrom
, userScheduleWeekTimeTo = userDefaultScheduleWeekTimeTo
, userScheduleWeekTimeslotLength = userDefaultScheduleWeekTimeslotLength
, userScheduleOccurrenceDisplayDefault = userDefaultScheduleOccurrenceDisplayDefault
, userTelephone = Nothing
, userMobile = Nothing
, userCompanyPersonalNumber = Nothing
@ -264,6 +292,7 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Just $ Languages ["sn"]
@ -275,6 +304,12 @@ fillDb = do
, userSex = Just SexNotApplicable
, userBirthday = Just $ n_day 3
, userShowSex = userDefaultShowSex
, userScheduleView = userDefaultScheduleView
, userScheduleWeekDays = userDefaultScheduleWeekDays
, userScheduleWeekTimeFrom = userDefaultScheduleWeekTimeFrom
, userScheduleWeekTimeTo = userDefaultScheduleWeekTimeTo
, userScheduleWeekTimeslotLength = userDefaultScheduleWeekTimeslotLength
, userScheduleOccurrenceDisplayDefault = userDefaultScheduleOccurrenceDisplayDefault
, userTelephone = Nothing
, userMobile = Nothing
, userCompanyPersonalNumber = Just "12345"
@ -291,7 +326,7 @@ fillDb = do
, userAuthentication = AuthLDAP
, userLastAuthentication = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing
, userMatrikelnummer = Just "11323801"
, userEmail = "vaupel.sarah@campus.lmu.de"
, userDisplayEmail = "vaupel.sarah@campus.lmu.de"
, userDisplayName = "Sarah Vaupel"
@ -300,10 +335,11 @@ fillDb = do
, userTitle = Nothing
, userMaxFavourites = 14
, userMaxFavouriteTerms = 4
, userTheme = ThemeMossGreen
, userTheme = ThemeNeutralBlue
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Nothing
@ -315,6 +351,12 @@ fillDb = do
, userSex = Just SexFemale
, userBirthday = Nothing
, userShowSex = userDefaultShowSex
, userScheduleView = userDefaultScheduleView
, userScheduleWeekDays = userDefaultScheduleWeekDays
, userScheduleWeekTimeFrom = userDefaultScheduleWeekTimeFrom
, userScheduleWeekTimeTo = userDefaultScheduleWeekTimeTo
, userScheduleWeekTimeslotLength = userDefaultScheduleWeekTimeslotLength
, userScheduleOccurrenceDisplayDefault = userDefaultScheduleOccurrenceDisplayDefault
, userTelephone = Nothing
, userMobile = Nothing
, userCompanyPersonalNumber = Nothing
@ -344,6 +386,7 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Nothing
@ -355,6 +398,12 @@ fillDb = do
, userSex = Just SexMale
, userBirthday = Nothing
, userShowSex = userDefaultShowSex
, userScheduleView = userDefaultScheduleView
, userScheduleWeekDays = userDefaultScheduleWeekDays
, userScheduleWeekTimeFrom = userDefaultScheduleWeekTimeFrom
, userScheduleWeekTimeTo = userDefaultScheduleWeekTimeTo
, userScheduleWeekTimeslotLength = userDefaultScheduleWeekTimeslotLength
, userScheduleOccurrenceDisplayDefault = userDefaultScheduleOccurrenceDisplayDefault
, userTelephone = Nothing
, userMobile = Nothing
, userCompanyPersonalNumber = Nothing
@ -544,6 +593,7 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Nothing
@ -555,6 +605,12 @@ fillDb = do
, userSex = Nothing
, userBirthday = Nothing
, userShowSex = userDefaultShowSex
, userScheduleView = userDefaultScheduleView
, userScheduleWeekDays = userDefaultScheduleWeekDays
, userScheduleWeekTimeFrom = userDefaultScheduleWeekTimeFrom
, userScheduleWeekTimeTo = userDefaultScheduleWeekTimeTo
, userScheduleWeekTimeslotLength = userDefaultScheduleWeekTimeslotLength
, userScheduleOccurrenceDisplayDefault = userDefaultScheduleOccurrenceDisplayDefault
, userTelephone = Nothing
, userMobile = Nothing
, userCompanyPersonalNumber = bool Nothing (Just "E123" ) (even $ length firstName)