Compare commits

..

3 Commits

Author SHA1 Message Date
3b0029ba04 fix(avs): fix #225 by skipping firm updates entirely if AVS FirmInfo is unchanged for previously seen values for AVS User to be updated 2024-10-09 12:50:32 +02:00
e554048f5a fix(avs): avs firm update no longer may update wrong company
Note: noticed while working on #225
2024-10-09 12:50:32 +02:00
e59fff352f fix(avs): fix #224 repeated superior changes no longer occur
furthermore AdminProblems are only inserted if the same problem does not exist unsolved
2024-10-09 12:50:32 +02:00
65 changed files with 209 additions and 1752 deletions

View File

@ -282,16 +282,9 @@ 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,13 +263,6 @@ 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
@ -287,8 +280,6 @@ 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,
@ -1703,63 +1694,6 @@ 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%
@ -1826,4 +1760,4 @@ form.schedule-options
color: var(--color-lightwhite)
&.nonactive
background-color: var(--color-nonactive)
color: var(--color-nonactive-dark)
color: var(--color-nonactive-dark)

View File

@ -2,6 +2,8 @@
#
# 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: —
@ -29,4 +31,4 @@ PaginationPage: Angzeigte Seite
PaginationError: Paginierung Parameter dürfen nicht negativ sein
NullDeletes: Zum Löschen NULL eingeben.
SortPriority: Sortierungsprioritätz
SortPriority: Sortierungspriorität

View File

@ -2,6 +2,8 @@
#
# 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 CourseName
name (CI Text)
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,17 +27,8 @@ 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 CourseEventType
course CourseId
type (CI Text)
course CourseId OnDeleteCascade OnUpdateCascade
room RoomReference Maybe
roomHidden Bool default=false
@ -45,12 +36,6 @@ 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,12 +47,6 @@ 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-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
-- SPDX-FileCopyrightText: 2022 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 TutorialType -- "Tutorium", "Zentralübung", ...
type (CI Text) -- "Tutorium", "Zentralübung", ...
capacity Int Maybe -- limit for enrolment in this tutorial
room RoomReference Maybe
roomHidden Bool default=false
@ -29,10 +29,4 @@ TutorialParticipant
user UserId
UniqueTutorialParticipant tutorial user
deriving Eq Ord Show
deriving Generic
TutorialScheduleOpt
tutorial TutorialId
user UserId
opt Bool
UniqueTutorialScheduleOpt tutorial user
deriving Generic
deriving Generic

View File

@ -1,4 +1,4 @@
-- 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-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-License-Identifier: AGPL-3.0-or-later
@ -32,7 +32,6 @@ 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
@ -41,12 +40,6 @@ 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,10 +170,7 @@
/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
@ -233,8 +230,6 @@
/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:
@ -246,8 +241,6 @@
/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
@ -260,11 +253,9 @@
!/download/*FilePath CNFileR GET !timeANDparticipant
!/events/add CEventsNewR GET POST
/events/#CryptoUUIDCourseEvent CourseEventR:
/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
/edit CEvEditR GET POST
/delete CEvDeleteR GET POST
/personalised-sheet-files CPersonalFilesR GET
/subs CorrectionsR GET POST !corrector !lecturer
@ -279,7 +270,6 @@
/msg/#{CryptoUUIDSystemMessage}/hide MessageHideR POST !timeANDauthentication
/schedule ScheduleR GET POST !free
/upload UploadR PUT !free

View File

@ -144,7 +144,6 @@ 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,7 +1,9 @@
-- SPDX-FileCopyrightText: 2023 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2023-24 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(..)
@ -17,6 +19,8 @@ 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
@ -129,7 +133,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
@ -173,20 +177,25 @@ 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
-- ^ Log a problem that needs interventions by admins, provided this problem has not already been reported and is still unsolved
--
-- - `problemLogTime` is now
-- - `problemSolver` is Nothing, we do not record the person who caused it
reportAdminProblem problem@(toJSON -> problemLogInfo) = do
problemLogTime <- liftIO getCurrentTime
reportAdminProblem problem = do
let problemLogSolved = Nothing
problemLogSolver = Nothing
insert_ ProblemLog{..}
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{..}
$logWarnS "Problem" $ Text.filter (/= '\n') $ tshow problem -- <> " - " <> pack (prettyCallStack callStack)

View File

@ -1,4 +1,4 @@
-- 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-FileCopyrightText: 2022-24 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, fromMaybe, guardMaybe, unsafeCoalesce
, maybe, maybe2, maybeEq, 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, fromMaybe, bool, max, min, abs)
import ClassyPrelude.Yesod hiding (isInfixOf, any, all, or, and, isJust, maybe, bool, max, min, abs)
import Data.Universe
import qualified Data.Set as Set
import qualified Data.List as List
@ -621,13 +621,6 @@ 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,7 +61,6 @@ import Data.CaseInsensitive (original, mk)
import qualified Data.Text as Text
import Utils.Form
import Utils.Schedule.Types.ScheduleView
import qualified GHC.Exts (IsList(..))
@ -504,13 +503,6 @@ 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-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
-- SPDX-FileCopyrightText: 2022 Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -25,7 +25,6 @@ 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-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-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-License-Identifier: AGPL-3.0-or-later
@ -265,8 +265,6 @@ 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
@ -278,8 +276,6 @@ 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
@ -300,8 +296,6 @@ 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
@ -313,8 +307,6 @@ 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
@ -377,8 +369,6 @@ 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-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-FileCopyrightText: 2022 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,12 +303,6 @@ 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,7 +25,6 @@ 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,4 +9,3 @@ 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

@ -1,36 +0,0 @@
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

@ -1,41 +0,0 @@
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,7 +10,6 @@ import Import
import Utils.Course
import Utils.Form
import Utils.Schedule
import Handler.Utils
import Handler.Utils.Course
import Handler.Utils.Tutorial
@ -30,16 +29,14 @@ import Handler.Exam.List (mkExamTable)
getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCShowR tid ssh csh = do
mbAuth <- maybeAuthPair
mbAid <- maybeAuthId
now <- liftIO getCurrentTime
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
(cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,administrators,correctors,tutors,news,events,submissionGroup,_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 (fst <$> mbAuth) E.==. participant E.?. CourseParticipantUser
E.&&. E.val mbAid 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
@ -99,30 +96,17 @@ 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 . view _1) mbAuth
let showRoom = maybe E.false (flip showCourseEventRoom courseEvent . E.val) mbAid
E.||. E.not_ (courseEvent E.^. CourseEventRoomHidden)
return (courseEvent, showRoom)
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)
events <- mapM (\(Entity evId ev, E.Value showRoom) -> (, ev, showRoom) <$> encrypt evId) events'
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 mbAuth $ \(uid,_) ->
submissionGroup' <- lift . for mbAid $ \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
@ -144,14 +128,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,mCourseScheduleOpt,mayReRegister,(mayViewSheets,mayViewAnySheet),(mayViewMaterials,mayViewAnyMaterial),courseQualifications)
return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,administrators,correctors,tutors,news,events,submissionGroup,mayReRegister, (mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial), courseQualifications)
mDereg <- traverse (formatTime SelFormatDateTime) $ courseDeregisterUntil course
regForm <- if
| is _Just mbAuth -> do
| is _Just mbAid -> do
(courseRegisterForm', regButton) <- courseRegisterForm (Entity cid course)
(regWidget, regEnctype) <- generateFormPost $ renderAForm FormStandard courseRegisterForm'
return $ wrapForm' regButton regWidget def
@ -175,7 +159,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 . view _1) mbAuth
let showRoom = maybe E.false (flip showTutorialRoom tutorial . E.val) mbAid
E.||. E.not_ (tutorial E.^. TutorialRoomHidden)
return (tutorial, showRoom)
dbtRowKey = (E.^. TutorialId)
@ -213,49 +197,22 @@ 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 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 -> 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 MsgTableActionsHead)) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) ->
cell $ linkButton mempty (msg2widget MsgMassRegister) [BCIsButton, BCPrimary] (SomeRoute $ CTutorialR tid ssh csh tutorialName TAddUserR)
]
@ -296,19 +253,12 @@ 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,7 +13,6 @@ 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

@ -1,45 +0,0 @@
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,14 +24,11 @@ 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
mAuth <- maybeAuth
ata <- getSessionActiveAuthTags
mUid <- maybeAuthId
(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
@ -57,27 +54,21 @@ 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 mAuth $ \(Entity uid _) -> E.select . E.from $ \examPartResult -> do
resultsRaw <- for mUid $ \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 mAuth $ getBy . UniqueExamResult eId . entityKey
bonus <- fmap join . for mAuth $ getBy . UniqueExamBonus eId . entityKey
result <- fmap join . for mUid $ getBy . UniqueExamResult eId
bonus <- fmap join . for mUid $ getBy . UniqueExamBonus eId
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
occurrencesRaw <- E.select . E.from $ \examOccurrence -> do
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 (Entity uid _) <- mAuth
| Just uid <- mUid
= E.exists . E.from $ \examRegistration ->
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eId
E.&&. examRegistration E.^. ExamRegistrationUser E.==. E.val uid
@ -88,22 +79,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 . entityKey) mAuth
showRoom = maybe E.false (flip showExamOccurrenceRoom examOccurrence . E.val) mUid
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, shouldBeDisplayedInSchedule, examOccurrenceScheduleOpt)
return (examOccurrence, registered, registeredCount, showRoom)
registeredCount <- fromIntegral <$> count [ ExamRegistrationExam ==. eId ]
registered <- for mAuth $ getBy . UniqueExamRegistration eId . entityKey
registered <- for mUid $ getBy . UniqueExamRegistration eId
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 _5 E.unValue . over _4 E.unValue . over _3 E.unValue . over _2 E.unValue) occurrencesRaw
let occurrences = sortOn sortPred $ map (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
@ -142,13 +133,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,27 +24,22 @@ 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
mUser <- maybeAuth
muid <- maybeAuthId
defaultLayout $ do
setTitleI MsgNewsHeading
newsSystemMessages
when (is _Nothing mUser) $
when (is _Nothing muid) $
notificationWidget NotificationBroad Info $(i18nWidgetFile "pitch")
case mUser of
Just user@(Entity uid _) -> do
newsSchedule user
case muid of
Just uid -> do
newsUpcomingExams uid
newsUpcomingSheets uid
Nothing ->
@ -98,14 +93,6 @@ 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,11 +44,8 @@ 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) #-}
@ -70,19 +67,12 @@ 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
@ -132,21 +122,8 @@ 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)
@ -157,7 +134,6 @@ 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
@ -180,12 +156,7 @@ 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 ]
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
themeList = [Option (toMessage t) t (toPathPiece t) | t <- universeF]
schoolsForm :: Maybe (Set SchoolId) -> AForm Handler (Set SchoolId)
schoolsForm template = formToAForm $ schoolsFormView =<< renderWForm FormStandard schoolsForm' mempty
@ -404,11 +375,6 @@ 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
@ -479,19 +445,12 @@ 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
@ -521,19 +480,12 @@ 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

View File

@ -1,105 +0,0 @@
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,6 +13,5 @@ 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

@ -1,40 +0,0 @@
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,6 +329,8 @@ 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
@ -380,72 +382,73 @@ updateAvsUserByADC newAvsDataContact@(AvsDataContact apid newAvsPersonInfo newAv
, UserAvsLastCardNo =. newAvsCardNo
]
-- 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
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
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
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
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
@ -585,16 +588,18 @@ 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 mbFirmEnt} new #{tshow newAvsFirmInfo}|]
case (mbFirmEnt, mbOldAvsFirmInfo) of
(Nothing, _) -> do -- insert new company, neither AvsId nor Shorthand exist in DB
$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
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 prodcued through firmInfo2company below for consistency
dmy = Company -- mostly dummy, values are actually produced through firmInfo2company below for consistency
{ companyName = newAvsFirmInfo ^. _avsFirmFirm . from _CI
, companyShorthand = newAvsFirmInfo ^. _avsFirmAbbreviation . from _CI
, companyAvsId = afn
@ -606,11 +611,12 @@ 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}, oldAvsFirmInfo) -> do -- possibly update existing company, if isJust oldAvsFirmInfo and changed occurred
let cmp_ups = mapMaybe (mkUpdate' firm newAvsFirmInfo oldAvsFirmInfo) firmInfo2company
(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
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}|]
@ -629,7 +635,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" "Update company completed."
$logInfoS "AVS" [st|Update company #{companyShorthand firm} completed.|]
return res_cmp2
where
firmInfo2key =
@ -645,8 +651,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 usrs =
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 =
let reasonSuperior = Just $ tshow SupervisorReasonAvsSuperior
getInsertUid eml = altM (guessUserByEmail $ stripCI eml) (catchAll2log' $ Just . entityKey <$> ldapLookupAndUpsert eml)
newAvsNo = newAfi ^. _avsFirmFirmNo
@ -655,22 +661,26 @@ upsertCompanySuperior Entity{entityKey=cid, entityVal=Company{}} newAfi oldAfi u
mbOldEmail = oldAfi ^? _Just . _avsFirmEMailSuperior . _Just
getSupId = getInsertUid `traverseJoin` mbSupEmail
getOldId = (guessUserByEmail . stripCI) `traverseJoin` mbOldEmail
unchangedCompany = oldAvsNo == Just newAvsNo
changedSuperior = mbSupEmail /= mbOldEmail -- beware, both could be Nothing
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
-- 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
mbSupId <- getSupId
mbUsrSup <- getSupervision mbSupId
-- delete old superiors, if any
when (unchangedCompany && changedSuperior) $
deleteWhere $ mcons ((UserSupervisorSupervisor !=.) <$> mbSupId)
[ UserSupervisorCompany ==. Just cid, UserSupervisorReason ==. reasonSuperior ]
unless unchangedCompany $
deleteWhere [ UserSupervisorReason ==. reasonSuperior, UserSupervisorUser <-. usrs ]
deleteWhere [ UserSupervisorReason ==. reasonSuperior, UserSupervisorUser ==. usrId ]
-- ensure superior supervision
case mbSupId of
Just supId -> do
case (mbSupId, mbUsrSup) of
(_ , Just _) -> return () -- supId is already supervisor for uid for any reason
(Just supId, Nothing) -> 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
@ -702,7 +712,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,16 +183,6 @@ 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
@ -1399,7 +1389,7 @@ dayTimeField fs mutc = do
fieldTimeFormat :: String
-- fieldTimeFormat = "%e.%m.%y %k:%M"
fieldTimeFormat = "%Y-%m-%dT%H:%M:%S%Q"
fieldTimeFormat = "%Y-%m-%dT%H:%M:%S"
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 & setTooltip MsgScheduleRegularTip)
(fslI MsgScheduleRegular)
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 performing for #{length linked} AVS linked users and #{length unlinked} unlinked users|]
$logInfoS "SynchronisAvs" [st|AVS synch start 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 performed for #{length linked} AVS linked users and #{length unlinked} unlinked users|]
$logInfoS "SynchronisAvs" [st|AVS synch end 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,8 +25,6 @@ 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-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-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-License-Identifier: AGPL-3.0-or-later
@ -47,18 +47,14 @@ 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-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-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-License-Identifier: AGPL-3.0-or-later
@ -34,8 +34,6 @@ import Web.HttpApiData
import Data.Aeson.Types as Aeson
import Model.Types.TH.PathPiece
----
-- Terms and anything loosely related to time
@ -229,18 +227,6 @@ 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-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-FileCopyrightText: 2022 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,8 +79,6 @@ 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
@ -284,15 +282,9 @@ 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,21 +831,6 @@ 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,7 +16,6 @@ module Utils.DateTime
, mkDateTimeFormatter
, nominalHour, nominalMinute
, minNominalYear, avgNominalYear
, nominalTimeToTimeOfDay, timeOfDayToNominalTime
, diffMinute, diffHour, diffDay
, module Zones
, day
@ -34,7 +33,6 @@ 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)
@ -164,11 +162,6 @@ 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,27 +425,6 @@ 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
@ -832,28 +811,6 @@ 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)
@ -1544,7 +1501,7 @@ hoistField f Field{..} = Field
}
prismAForm :: Monad m => Prism' s a -> Maybe s -> (Maybe a -> AForm m a) -> AForm m s
-- ^ TODO: @Monad m => Prism' s a -> (Maybe a -> AForm m a) -> (Maybe s -> AForm m s)@
-- ^ @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-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-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-License-Identifier: AGPL-3.0-or-later
@ -96,11 +96,6 @@ data Icon
| IconFileUploadSession
| IconStandaloneFieldError
| IconFileUser
| IconFastBackward
| IconBackward
| IconCurrent
| IconForward
| IconFastForward
| IconPersonalIdentification
| IconMenuWorkflows
| IconVideo
@ -199,11 +194,6 @@ 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,7 +37,6 @@ data GlobalGetParam = GetLang
| GetError
| GetSelectTable
| GetGenerateToken
| GetScheduleOptions
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
deriving anyclass (Universe, Finite)

View File

@ -1,128 +0,0 @@
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

@ -1,37 +0,0 @@
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

@ -1,34 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,45 +0,0 @@
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

@ -1,23 +0,0 @@
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

View File

@ -1,261 +0,0 @@
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

@ -1,34 +0,0 @@
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

@ -1,67 +0,0 @@
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

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

View File

@ -1,14 +0,0 @@
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)

View File

@ -1,25 +0,0 @@
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,16 +243,12 @@ $# $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, courseEventCurrentOpt, mEventScheduleOpt) <- events
$forall (cID, CourseEvent{courseEventType, courseEventTime, courseEventRoom, courseEventNote}, showRoom) <- events
<tr .table__row ##{"event-" <> toPathPiece cID}>
<td .table__td>
<div .table__td-content>
@ -273,15 +269,6 @@ $# $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>
@ -306,12 +293,3 @@ $# $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__th>
<th .table__td>
$if not occurrenceAssignmentsVisible
^{isVisible False}
$if showRegisteredCount
@ -232,11 +232,8 @@ $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, shouldBeDisplayedInSchedule, mEOScheduleOpt) <- occurrences
$forall (occurrence, registered, rCount, showRoom) <- 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>
@ -271,14 +268,6 @@ $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

@ -1,17 +0,0 @@
$# 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

@ -1,18 +0,0 @@
$# 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

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

View File

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

View File

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

View File

@ -1,77 +0,0 @@
$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

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

View File

@ -0,0 +1,7 @@
$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-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-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-License-Identifier: AGPL-3.0-or-later
@ -98,7 +98,6 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Just $ Languages ["en"]
@ -110,12 +109,6 @@ 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"
@ -145,7 +138,6 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Nothing
@ -156,12 +148,6 @@ 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
@ -198,7 +184,6 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Nothing
@ -210,12 +195,6 @@ 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"
@ -245,7 +224,6 @@ fillDb = do
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Just $ Languages ["de"]
@ -257,12 +235,6 @@ 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
@ -292,7 +264,6 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Just $ Languages ["sn"]
@ -304,12 +275,6 @@ 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"
@ -326,7 +291,7 @@ fillDb = do
, userAuthentication = AuthLDAP
, userLastAuthentication = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Just "11323801"
, userMatrikelnummer = Nothing
, userEmail = "vaupel.sarah@campus.lmu.de"
, userDisplayEmail = "vaupel.sarah@campus.lmu.de"
, userDisplayName = "Sarah Vaupel"
@ -335,11 +300,10 @@ fillDb = do
, userTitle = Nothing
, userMaxFavourites = 14
, userMaxFavouriteTerms = 4
, userTheme = ThemeNeutralBlue
, userTheme = ThemeMossGreen
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Nothing
@ -351,12 +315,6 @@ 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
@ -386,7 +344,6 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Nothing
@ -398,12 +355,6 @@ 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
@ -593,7 +544,6 @@ fillDb = do
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userWeekStart = userDefaultWeekStart
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userLanguages = Nothing
@ -605,12 +555,6 @@ 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)