From 20b3a39bc3e35f3b1b238c1fa4c35d9350d252a5 Mon Sep 17 00:00:00 2001 From: Steffen Jost Date: Fri, 21 Jul 2023 15:47:57 +0000 Subject: [PATCH] chore(lms): WIP V2 Report pages done, job and lms-main page to do --- .../categories/qualification/de-de-formal.msg | 16 ++- .../categories/qualification/en-eu.msg | 12 ++- models/lms.model | 8 +- src/Handler/LMS/Report.hs | 102 ++++++++++++------ src/Jobs/Handler/LMS.hs | 2 + src/Model/Types/Lms.hs | 22 ++-- templates/lms-report.hamlet | 11 ++ test/Database/Fill.hs | 12 +-- 8 files changed, 127 insertions(+), 58 deletions(-) create mode 100644 templates/lms-report.hamlet diff --git a/messages/uniworx/categories/qualification/de-de-formal.msg b/messages/uniworx/categories/qualification/de-de-formal.msg index 013a76a8a..b19a8eba5 100644 --- a/messages/uniworx/categories/qualification/de-de-formal.msg +++ b/messages/uniworx/categories/qualification/de-de-formal.msg @@ -49,6 +49,7 @@ TableLmsElearning: E‑Learning TableLmsPin: E‑Learning Passwort TableLmsResetPin: E-Learning Passwort zurücksetzen? TableLmsDatePin: E-Learning Passwort erstellt +TableLmsDate: Datum TableLmsDelete: Löschen? TableLmsStaff: Interner Mitarbeiter? TableLmsStarted: Begonnen @@ -76,15 +77,20 @@ CsvColumnLmsPin: Passwort E#{nonBreakableDash}Learning Zugang CsvColumnLmsResetPin: Wird das E-Learning Passwort bei der nächsten Synchronisation zurückgesetzt? CsvColumnLmsDelete: Wird der Identifikator in der E‑Learning Plattform bei der nächsten Synchronisation gelöscht? CsvColumnLmsStaff: Handelt es sich um einen internen Mitarbeiter? (Aus historischen Gründen, wird momentan ignoriert.) -CsvColumnLmsSuccess: Zeitstempel der erfolgreichen Teilnahme (UTC) +CsvColumnLmsSuccess: Zeitstempel der erfolgreichen Teilnahme +CsvColumnLmsDate: Datum des E‑Learning Ereignisses CsvColumnLmsResetTries: Anzahl der bisher verbrauchten E‑Learning Prüfungsversuche zurücksetzen CsvColumnLmsLock: E‑Learning Login gesperrt +CsvColumnLmsResult !ident-ok: LMS Status LmsUserlistInsert: Neuer LMS User -LmsUserlistUpdate: LMS User aktualisierung +LmsUserlistUpdate: LMS User Aktualisierung LmsResultInsert: Neues LMS Ergebnis -LmsResultUpdate: LMS Ergebnis aktualisierung -LmsResultCsvExceptionDuplicatedKey: CSV Import fand uneindeutigen Schlüssel -LmsUserlistCsvExceptionDuplicatedKey: CSV Import fand uneindeutigen Schlüssel +LmsResultUpdate: LMS Ergebnis Aktualisierung +LmsReportInsert: Neues LMS Ereignis +LmsReportUpdate: LMS Ereignis Aktualisierung +LmsResultCsvExceptionDuplicatedKey: CSV-Import LmsResult fand uneindeutigen Schlüssel +LmsUserlistCsvExceptionDuplicatedKey: CSV-Import LmsUserlist fand uneindeutigen Schlüssel +LmsReportCsvExceptionDuplicatedKey: CSV-Import LmsReport fand uneindeutigen Schlüssel LmsDirectUpload: Direkter Upload für automatisierte Systeme LmsErrorNoRefreshElearning: Fehler: E‑Learning wird nicht automatisch gestartet, da die Zeitspanne für den Erneurerungszeitraum nicht festgelegt wurde. MailSubjectQualificationRenewal qname@Text: Qualifikation #{qname} muss demnächst erneuert werden diff --git a/messages/uniworx/categories/qualification/en-eu.msg b/messages/uniworx/categories/qualification/en-eu.msg index 3d3e4a00b..f13dff6fd 100644 --- a/messages/uniworx/categories/qualification/en-eu.msg +++ b/messages/uniworx/categories/qualification/en-eu.msg @@ -49,6 +49,7 @@ TableLmsPin: E‑learning password TableLmsElearning: E‑learning TableLmsResetPin: Reset E‑learning password? TableLmsDatePin: E‑learning password created +TableLmsDate: Date TableLmsDelete: Delete? TableLmsStaff: Staff? TableLmsStarted: Started @@ -74,17 +75,22 @@ FilterLmsNotificationDue: Notification due CsvColumnLmsIdent: E#{nonBreakableDash}learning identifier, unique for each qualification and user CsvColumnLmsPin: Password e#{nonBreakableDash}learning access CsvColumnLmsResetPin: Will the e#{nonBreakableDash}learning password be reset upon next synchronisation? -CsvColumnLmsDelete: Will the identifier be deleted from the E‑learning platfrom upon next synchronisation? +CsvColumnLmsDelete: Will the identifier be deleted from the e‑learning platfrom upon next synchronisation? CsvColumnLmsStaff: Is the user an internal staff member? (Legacy, currently ignored) CsvColumnLmsSuccess: Timestamp of successful completion (UTC) CsvColumnLmsResetTries: Reset number of used up e‑learning exam attempts +CsvColumnLmsDate: Date of e‑learning event +CsvColumnLmsResult: LMS Status CsvColumnLmsLock: E‑learning login is not permitted LmsUserlistInsert: New LMS user LmsUserlistUpdate: Update of LMS user LmsResultInsert: New LMS result LmsResultUpdate: Update of LMS result -LmsResultCsvExceptionDuplicatedKey: CSV import with ambiguous key -LmsUserlistCsvExceptionDuplicatedKey: CSV import with ambiguous key +LmsReportInsert: New LMS event +LmsReportUpdate: Update of LMS event +LmsResultCsvExceptionDuplicatedKey: CSV import LmsResult with ambiguous key +LmsUserlistCsvExceptionDuplicatedKey: CSV import LmsUserlist with ambiguous key +LmsReportCsvExceptionDuplicatedKey: CSV Import LmsReport with ambiguous key LmsDirectUpload: Direct upload for automated systems LmsErrorNoRefreshElearning: Error: E‑learning will not be started automatically due to refresh-within time period not being set. MailSubjectQualificationRenewal qname: Qualification #{qname} must be renewed shortly diff --git a/models/lms.model b/models/lms.model index a77e63df0..db9dc54e7 100644 --- a/models/lms.model +++ b/models/lms.model @@ -113,7 +113,7 @@ LmsUser user UserId OnDeleteCascade OnUpdateCascade ident LmsIdent -- must be unique accross all LMS courses! pin Text - resetPin Bool default=false -- should pin be reset? + resetPin Bool default=false -- should pin be reset? datePin UTCTime default=now() -- time pin was created status LmsStatus Maybe -- open, success or failure; status should never change unless isNothing; isJust indicates lms is finished and user shall be deleted from LMS --toDelete encoded by Handler.Utils.LMS.lmsUserToDelete @@ -122,7 +122,9 @@ LmsUser received UTCTime Maybe -- last acknowledgement by LMS notified UTCTime Maybe -- last notified by FRADrive ended UTCTime Maybe -- ident was deleted from LMS - -- Primary ident -- newtype Key LmsUserId = LmsUserKey { unLmsUser :: Text } -- change LmsIdent -> Text. Do we want this? + resetTries Bool default=false -- V2 should e-learning exam tries be reset? + locked Bool default=false -- V2 last returned lock status + -- Primary ident -- newtype Key LmsUserId = LmsUserKey { unLmsUser :: Text } -- change LmsIdent -> Text. Do we want this? No. UniqueLmsIdent ident -- idents must be unique accross all qualifications, since idents are global within LMS! UniqueLmsQualificationUser qualification user -- each user may be enrolled at most once per course deriving Generic @@ -156,7 +158,7 @@ LmsReport qualification QualificationId OnDeleteCascade OnUpdateCascade ident LmsIdent date Day Maybe -- BEWARE: timezone is local as submitted by LMS - result Int -- (0|1|2) 0=too many ties, 1=open, 2=success + result LmsState -- (0|1|2) 0=too many ties, 1=open, 2=success lock Bool -- (0|1) timestamp UTCTime default=now() UniqueLmsReport qualification ident -- required by DBTable diff --git a/src/Handler/LMS/Report.hs b/src/Handler/LMS/Report.hs index aabcb1367..2536fa794 100644 --- a/src/Handler/LMS/Report.hs +++ b/src/Handler/LMS/Report.hs @@ -70,6 +70,20 @@ instance CsvColumnsExplained LmsReportTableCsv where single :: RenderMessage UniWorX msg => Csv.Name -> msg -> Map Csv.Name Widget single k v = singletonMap k [whamlet|_{v}|] +data LmsReportCsvActionClass = LmsReportInsert | LmsReportUpdate + deriving (Eq, Ord, Read, Show, Generic, Enum, Bounded) +embedRenderMessage ''UniWorX ''LmsReportCsvActionClass id + +-- By coincidence the action type is identical to LmsReportTableCsv +data LmsReportCsvAction = LmsReportInsertData { lmsReportCsvIdent :: LmsIdent, lmsReportCsvDate :: Maybe Day, lmsReportCsvResult :: LmsState, lmsReportCsvLock :: Bool } + | LmsReportUpdateData { lmsReportCsvIdent :: LmsIdent, lmsReportCsvDate :: Maybe Day, lmsReportCsvResult :: LmsState, lmsReportCsvLock :: Bool } + deriving (Eq, Ord, Read, Show, Generic) + +deriveJSON defaultOptions + { constructorTagModifier = camelToPathPiece'' 2 1 -- LmsReportInsertData -> insert + , fieldLabelModifier = camelToPathPiece' 2 -- lmsReportCsvIdent -> csv-ident + , sumEncoding = TaggedObject "action" "data" + } ''LmsReportCsvAction data LmsReportCsvException = LmsReportCsvExceptionDuplicatedKey -- TODO: this is not used anywhere?! @@ -95,8 +109,8 @@ mkReportTable sid qsh qid = do [ sortable (Just csvLmsIdent) (i18nCell MsgTableLmsIdent) $ \(view $ _dbrOutput . _entityVal . _lmsReportIdent . _getLmsIdent -> ident) -> textCell ident , sortable (Just csvLmsDate) (i18nCell MsgTableLmsDate) $ \(view $ _dbrOutput . _entityVal . _lmsReportDate -> d) -> cellMaybe dayCell d , sortable (Just csvLmsResult) (i18nCell MsgTableLmsStatus) $ \(view $ _dbrOutput . _entityVal . _lmsReportResult -> s) -> lmsStateCell s - , sortable (Just csvLmsLock) (i18nCell MsgTableLmsLock) $ \(view $ _dbrOutput . _entityVal . _lmsReportLock . _lmsBool -> b) -> ifIconCell b IconLocked - , sortable (Just csvLmsTimestamp) (i18nCell MsgTableLmsReceived)$ \(view $ _dbrOutput . _entityVal . _lmsReportTimestamp -> t) -> dateTimeCell timestamp + , sortable (Just csvLmsLock) (i18nCell MsgTableLmsLock) $ \(view $ _dbrOutput . _entityVal . _lmsReportLock -> b) -> ifIconCell b IconLocked + , sortable (Just csvLmsTimestamp) (i18nCell MsgTableLmsReceived)$ \(view $ _dbrOutput . _entityVal . _lmsReportTimestamp -> t) -> dateTimeCell t ] dbtSorting = Map.fromList [ (csvLmsIdent , SortColumn (E.^. LmsReportIdent)) @@ -128,7 +142,7 @@ mkReportTable sid qsh qid = do [ LmsReportTableCsv { csvLRident = LmsIdent lid , csvLRdate = LmsDay $ addDays (-dos) now_day - , csvLRresult = LmsState $ toEnum $ dos `mod` succ (fromEnum (maxBound :: LmsState)) + , csvLRresult = toEnum $ dos `mod` succ (fromEnum (maxBound :: LmsState)) , csvLRlock = LmsBool $ even dos } | (lid,dos) <- zip ["abcdefgh", "12345678", "ident8ch", "x2!y3-z4"] [1..] @@ -136,26 +150,30 @@ mkReportTable sid qsh qid = do } where doEncode' = LmsReportTableCsv - <$> view (_dbrOutput . _entityVal . _lmsReportIdent) - <*> view (_dbrOutput . _entityVal . _lmsReportDate . _lmsDay) - <*> view (_dbrOutput . _entityVal . _lmsReportResult . enum) - <*> view (_dbrOutput . _entityVal . _lmsReportLock . _lmsBool) + <$> view (_dbrOutput . _entityVal . _lmsReportIdent) + <*> preview (_dbrOutput . _entityVal . _lmsReportDate . _Just . _lmsDay) + <*> view (_dbrOutput . _entityVal . _lmsReportResult) + <*> view (_dbrOutput . _entityVal . _lmsReportLock . _lmsBool) dbtCsvDecode = Just DBTCsvDecode -- Just save to DB; Job will process data later { dbtCsvRowKey = \LmsReportTableCsv{..} -> - fmap E.Value . MaybeT . getKeyBy $ UniqueLmsReport qid csvLRTident + fmap E.Value . MaybeT . getKeyBy $ UniqueLmsReport qid csvLRident , dbtCsvComputeActions = \case -- purpose is to show a diff to the user first - DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do - yield $ LmsReportInsertData - { lmsReportInsertIdent = csvLRTident dbCsvNew - , lmsReportInsertSuccess = csvLRTsuccess dbCsvNew & lms2day - } + DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew = LmsReportTableCsv{..}} -> do + yield $ LmsReportInsertData + { lmsReportCsvIdent = csvLRident + , lmsReportCsvDate = csvLRdate <&> lms2day + , lmsReportCsvResult = csvLRresult + , lmsReportCsvLock = csvLRlock + } DBCsvDiffNew{dbCsvNewKey = Just _, dbCsvNew = _} -> error "UniqueLmsReport was found, but the key no longer exists." -- TODO: how can this ever happen? Check Pagination-Code DBCsvDiffExisting{dbCsvNew = LmsReportTableCsv{..}, dbCsvOld} -> do - let successDay = lms2day csvLRTsuccess - when (successDay /= dbCsvOld ^. _dbrOutput . _entityVal . _lmsReportSuccess) $ + let resultDay = csvLRdate <&> lms2day + when (resultDay > dbCsvOld ^. _dbrOutput . _entityVal . _lmsReportDate) $ yield $ LmsReportUpdateData - { lmsReportInsertIdent = csvLRTident - , lmsReportInsertSuccess = successDay + { lmsReportCsvIdent = csvLRident + , lmsReportCsvDate = resultDay + , lmsReportCsvResult = csvLRresult + , lmsReportCsvLock = csvLRlock } DBCsvDiffMissing{} -> return () -- no deletion , dbtCsvClassifyAction = \case @@ -171,37 +189,49 @@ mkReportTable sid qsh qid = do void $ upsert LmsReport { lmsReportQualification = qid - , lmsReportIdent = lmsReportInsertIdent actionData - , lmsReportSuccess = lmsReportInsertSuccess actionData - , lmsReportTimestamp = now -- lmsReportInsertTimestamp -- does it matter which one to choose? + , lmsReportIdent = lmsReportCsvIdent actionData + , lmsReportDate = lmsReportCsvDate actionData + , lmsReportResult = lmsReportCsvResult actionData + , lmsReportLock = lmsReportCsvLock actionData + , lmsReportTimestamp = now } - [ LmsReportSuccess =. lmsReportInsertSuccess actionData + [ LmsReportDate =. lmsReportCsvDate actionData + , LmsReportResult =. lmsReportCsvResult actionData + , LmsReportLock =. lmsReportCsvLock actionData , LmsReportTimestamp =. now ] -- audit $ Transaction.. (add to Audit.Types) - lift . queueDBJob $ JobLmsReports qid + lift . queueDBJob $ JobLmsReports qid -- TODO: V2 return $ LmsReportR sid qsh , dbtCsvRenderKey = const $ \case LmsReportInsertData{..} -> do -- TODO: i18n [whamlet| $newline never - Insert: Ident #{getLmsIdent lmsReportInsertIdent} # - had success on ^{formatTimeW SelFormatDate lmsReportInsertSuccess} + Insert: Ident #{getLmsIdent lmsReportCsvIdent} # + has status #{show lmsReportCsvResult} # + $if lmsReportCsvLock + and is locked # + $maybe d <- lmsReportCsvDate + on ^{formatTimeW SelFormatDate d} |] LmsReportUpdateData{..} -> do -- TODO: i18n [whamlet| $newline never - Update: Ident #{getLmsIdent lmsReportInsertIdent} # - had success on ^{formatTimeW SelFormatDate lmsReportInsertSuccess} + Update: Ident #{getLmsIdent lmsReportCsvIdent} # + has status #{show lmsReportCsvResult} # + $if lmsReportCsvLock + and is locked # + $maybe d <- lmsReportCsvDate + on ^{formatTimeW SelFormatDate d} |] , dbtCsvRenderActionClass = toWidget <=< ap getMessageRender . pure , dbtCsvRenderException = ap getMessageRender . pure :: LmsReportCsvException -> DB Text } dbtExtraReps = [] - ReportDBTableValidator = def + reportDBTableValidator = def & defaultSorting [SortAscBy csvLmsIdent] - dbTable ReportDBTableValidator ReportDBTable + dbTable reportDBTableValidator reportDBTable getLmsReportR, postLmsReportR :: SchoolId -> QualificationShorthand -> Handler Html getLmsReportR = postLmsReportR @@ -212,7 +242,7 @@ postLmsReportR sid qsh = do view _2 <$> mkReportTable sid qsh qid siteLayoutMsg MsgMenuLmsReport $ do setTitleI MsgMenuLmsReport - $(widgetFile "lms-Report") + $(widgetFile "lms-report") -- Direct File Upload/Download @@ -223,11 +253,15 @@ saveReportCsv qid i LmsReportTableCsv{..} = do void $ upsert LmsReport { lmsReportQualification = qid - , lmsReportIdent = csvLRTident - , lmsReportSuccess = csvLRTsuccess & lms2day + , lmsReportIdent = csvLRident + , lmsReportDate = csvLRdate <&> lms2day + , lmsReportResult = csvLRresult + , lmsReportLock = csvLRlock , lmsReportTimestamp = now } - [ LmsReportSuccess =. (csvLRTsuccess & lms2day) + [ LmsReportDate =. (csvLRdate <&> lms2day) + , LmsReportResult =. csvLRresult + , LmsReportLock =. csvLRlock , LmsReportTimestamp =. now ] return $ succ i @@ -238,8 +272,8 @@ makeReportUploadForm = renderAForm FormStandard $ fileAFormReq "Report CSV" getLmsReportUploadR, postLmsReportUploadR :: SchoolId -> QualificationShorthand -> Handler Html getLmsReportUploadR = postLmsReportUploadR postLmsReportUploadR sid qsh = do - ((Report,widget), enctype) <- runFormPost makeReportUploadForm - case Report of + ((report,widget), enctype) <- runFormPost makeReportUploadForm + case report of FormSuccess file -> do -- content <- fileSourceByteString file -- return $ Just (fileName file, content) diff --git a/src/Jobs/Handler/LMS.hs b/src/Jobs/Handler/LMS.hs index 83f44e556..8264fdba0 100644 --- a/src/Jobs/Handler/LMS.hs +++ b/src/Jobs/Handler/LMS.hs @@ -146,6 +146,8 @@ dispatchJobLmsEnqueueUser qid uid = JobHandlerAtomic act , lmsUserReceived = Nothing , lmsUserNotified = Nothing , lmsUserEnded = Nothing + , lmsUserResetTries = False + , lmsUserLocked = True -- initially display locked, since it is not yet available until the first feedback } -- startLmsUser :: YesodJobDB UniWorX (Maybe (Entity LmsUser)) startLmsUser = do diff --git a/src/Model/Types/Lms.hs b/src/Model/Types/Lms.hs index ecdc25d37..885c08311 100644 --- a/src/Model/Types/Lms.hs +++ b/src/Model/Types/Lms.hs @@ -24,7 +24,7 @@ import Utils.Lens.TH newtype LmsIdent = LmsIdent { getLmsIdent :: Text } deriving (Eq, Ord, Read, Show, Generic) deriving newtype (NFData, PathPiece, PersistField, PersistFieldSql, Csv.ToField, Csv.FromField, Hashable) -instance E.SqlString LmsIdent +instance E.SqlString LmsIdent makeLenses_ ''LmsIdent deriveJSON defaultOptions @@ -35,16 +35,16 @@ deriveJSON defaultOptions -- TODO: Is this a good idea? An ordinary Enum and a separate Day column in the DB would be better, e.g. allowing use of insertSelect in Jobs.Handler.LMS? -- ...also see similar type QualificationBlocked -data LmsStatus = LmsExpired - | LmsBlocked - | LmsSuccess +data LmsStatus = LmsExpired + | LmsBlocked + | LmsSuccess deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, NFData, Universe, Finite) -- embedRenderMessage ''UniWorX ''LmsStatus (uncurry ((<>) . (<> "Status")) . Text.splitAt 3) -- neccessarily moved to Handler.Utils.Lms deriveJSON defaultOptions { constructorTagModifier = camelToPathPiece' 1 -- remove lms from constructor - , fieldLabelModifier = camelToPathPiece' 1 + , fieldLabelModifier = camelToPathPiece' 1 , sumEncoding = UntaggedValue } ''LmsStatus derivePersistFieldJSON ''LmsStatus @@ -53,7 +53,6 @@ nullaryPathPiece ''LmsStatus $ camelToPathPiece' 1 instance Csv.ToField LmsStatus where toField = Csv.toField . toPathPiece - -- | Default Block/Unblock reasons @@ -93,7 +92,7 @@ instance Csv.FromField LmsBool where parseField "1" = pure $ LmsBool True parseField _ = mempty --- | LMS interface communicating user status +-- | Only to be used in LMS interface communicating user status data LmsState = LmsFailed | LmsOpen | LmsPassed deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, NFData, Universe, Finite) @@ -108,6 +107,15 @@ instance Csv.FromField LmsState where parseField "2" = pure LmsPassed parseField _ = mempty +deriveJSON defaultOptions + { constructorTagModifier = camelToPathPiece' 1 -- remove lms from constructor + , fieldLabelModifier = camelToPathPiece' 1 + , sumEncoding = UntaggedValue + } ''LmsState +derivePersistFieldJSON ''LmsState +nullaryPathPiece ''LmsState $ camelToPathPiece' 1 + + -- | LMS interface requires day format not compliant with iso8601; also LMS uses LOCAL TIMEZONE newtype LmsDay = LmsDay { lms2day :: Day } deriving (Eq, Ord, Read, Show, Generic) diff --git a/templates/lms-report.hamlet b/templates/lms-report.hamlet new file mode 100644 index 000000000..dc4a84f5c --- /dev/null +++ b/templates/lms-report.hamlet @@ -0,0 +1,11 @@ +$newline never + +$# SPDX-FileCopyrightText: 2022 Steffen Jost +$# +$# SPDX-License-Identifier: AGPL-3.0-or-later + +

+ ^{lmsTable} +

+ + _{MsgLmsDirectUpload} diff --git a/test/Database/Fill.hs b/test/Database/Fill.hs index 03c7ee385..d8b55b837 100644 --- a/test/Database/Fill.hs +++ b/test/Database/Fill.hs @@ -731,12 +731,12 @@ fillDb = do void . insert' $ LmsUserlist qid_f (LmsIdent "hijklmn") False now void . insert' $ LmsUserlist qid_f (LmsIdent "abcdefg") True now void . insert' $ LmsUserlist qid_f (LmsIdent "ijk" ) False now - void . insert' $ LmsUser qid_f jost (LmsIdent "ijk" ) "123" False now Nothing Nothing now Nothing (Just $ n_day' (-7)) (Just $ n_day' (-5)) - void . insert' $ LmsUser qid_f svaupel (LmsIdent "abcdefg") "abc" False now (Just LmsSuccess) (Just $ n_day 1) (n_day' (-1)) (Just now) (Just $ n_day' 0) Nothing - void . insert' $ LmsUser qid_f gkleen (LmsIdent "hijklmn") "@#!" True now (Just LmsBlocked) (Just $ utctDay now) (n_day' (-2)) (Just now) (Just $ n_day' (-4)) Nothing - void . insert' $ LmsUser qid_f tinaTester (LmsIdent "qwvu") "45678" True now (Just LmsSuccess) (Just $ n_day (-22)) (n_day' (-3)) (Just $ n_day' (-1)) (Just $ n_day' (-1)) Nothing - void . insert' $ LmsUser qid_f maxMuster (LmsIdent "xyz") "a1b2c3" False now (Just LmsBlocked) (Just $ n_day (-11)) (n_day' (-4)) (Just $ n_day' (-2)) (Just $ n_day' (-2)) Nothing - void . insert' $ LmsUser qid_f fhamann (LmsIdent "123") "456" False now Nothing Nothing now Nothing Nothing Nothing + void . insert' $ LmsUser qid_f jost (LmsIdent "ijk" ) "123" False now Nothing Nothing now Nothing (Just $ n_day' (-7)) (Just $ n_day' (-5)) False False + void . insert' $ LmsUser qid_f svaupel (LmsIdent "abcdefg") "abc" False now (Just LmsSuccess) (Just $ n_day 1) (n_day' (-1)) (Just now) (Just $ n_day' 0) Nothing True False + void . insert' $ LmsUser qid_f gkleen (LmsIdent "hijklmn") "@#!" True now (Just LmsBlocked) (Just $ utctDay now) (n_day' (-2)) (Just now) (Just $ n_day' (-4)) Nothing False True + void . insert' $ LmsUser qid_f tinaTester (LmsIdent "qwvu") "45678" True now (Just LmsSuccess) (Just $ n_day (-22)) (n_day' (-3)) (Just $ n_day' (-1)) (Just $ n_day' (-1)) Nothing True True + void . insert' $ LmsUser qid_f maxMuster (LmsIdent "xyz") "a1b2c3" False now (Just LmsBlocked) (Just $ n_day (-11)) (n_day' (-4)) (Just $ n_day' (-2)) (Just $ n_day' (-2)) Nothing True True + void . insert' $ LmsUser qid_f fhamann (LmsIdent "123") "456" False now Nothing Nothing now Nothing Nothing Nothing False False void . insert $ PrintJob "TestJob1" "AckTestJob1" "job1" "No Text herein." (n_day' (-1)) Nothing Nothing (Just svaupel) Nothing (Just qid_f) Nothing void . insert $ PrintJob "TestJob2" "AckTestJob2" "job2" "No Text herein." (n_day' (-3)) (Just $ n_day' (-1)) (Just jost) (Just svaupel) Nothing (Just qid_f) (Just $ LmsIdent "ijk")