From 0f519050ebfeb4f36caef44ad0c65773161a57b5 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel <> Date: Tue, 11 Aug 2020 10:00:42 +0200 Subject: [PATCH 01/31] refactor: move correct interfaces to utils --- src/Handler/Exam/Correct.hs | 59 +------------------------------ src/Utils/Exam.hs | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 58 deletions(-) create mode 100644 src/Utils/Exam.hs diff --git a/src/Handler/Exam/Correct.hs b/src/Handler/Exam/Correct.hs index 895d72d86..c74970da7 100644 --- a/src/Handler/Exam/Correct.hs +++ b/src/Handler/Exam/Correct.hs @@ -6,7 +6,6 @@ import Import import qualified Data.Set as Set import qualified Data.CaseInsensitive as CI -import qualified Data.Aeson as JSON import qualified Database.Esqueleto as E import qualified Database.Esqueleto.Utils as E @@ -15,63 +14,7 @@ import Database.Persist.Sql (transactionUndo) import Handler.Utils import Handler.Utils.Exam (fetchExam) -import qualified Data.HashMap.Strict as HashMap - - -data CorrectInterfaceUser - = CorrectInterfaceUser - { ciuSurname :: Text - , ciuDisplayName :: Text - , ciuMatNr :: Maybe UserMatriculation - , ciuId :: CryptoUUIDUser - } deriving (Eq,Ord) -deriveJSON defaultOptions - { fieldLabelModifier = camelToPathPiece' 1 - } ''CorrectInterfaceUser - -data CorrectInterfaceResponse - = CorrectInterfaceResponseSuccess - { cirsUser :: CorrectInterfaceUser - , cirsResults :: Map ExamPartNumber (Maybe ExamResultPoints) - , cirsGrade :: Maybe (Maybe ExamResultPassedGrade) - , cirsTime :: UTCTime - } - | CorrectInterfaceResponseAmbiguous - { ciraUsers :: Set CorrectInterfaceUser - , ciraMessage :: Text - } - | CorrectInterfaceResponseFailure - { cirfUser :: Maybe CorrectInterfaceUser - , cirfMessage :: Text - } - | CorrectInterfaceResponseNoOp - { cirnUsers :: Set CorrectInterfaceUser - } -deriveToJSON defaultOptions - { constructorTagModifier = camelToPathPiece' 3 - , fieldLabelModifier = camelToPathPiece' 1 - , sumEncoding = TaggedObject "status" "results" - , omitNothingFields = True - } ''CorrectInterfaceResponse - -data CorrectInterfaceRequest - = CorrectInterfaceRequest - { ciqUser :: Either Text (CryptoID UUID (Key User)) - , ciqResults :: Maybe (NonNull (Map ExamPartNumber (Maybe Points))) - , ciqGrade :: Maybe (Maybe ExamResultPassedGrade) - } - -instance FromJSON CorrectInterfaceRequest where - parseJSON = JSON.withObject "CorrectInterfaceRequest" $ \o -> do - ciqUser <- Right <$> o JSON..: "user" <|> Left <$> o JSON..: "user" - results <- o JSON..:? "results" - ciqResults <- for results $ maybe (fail "Results may not be nullable") return . fromNullable - ciqGrade <- if - | "grade" `HashMap.member` o - -> Just <$> o JSON..: "grade" - | otherwise - -> pure Nothing - return CorrectInterfaceRequest{..} +import Utils.Exam getECorrectR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html diff --git a/src/Utils/Exam.hs b/src/Utils/Exam.hs new file mode 100644 index 000000000..bca2727ca --- /dev/null +++ b/src/Utils/Exam.hs @@ -0,0 +1,70 @@ +module Utils.Exam + ( CorrectInterfaceRequest(..) + , CorrectInterfaceResponse(..) + , CorrectInterfaceUser(..) + ) where + +import Import.NoFoundation + +import qualified Data.Aeson as JSON +import qualified Data.HashMap.Strict as HashMap + + +data CorrectInterfaceUser + = CorrectInterfaceUser + { ciuSurname :: Text + , ciuDisplayName :: Text + , ciuMatNr :: Maybe UserMatriculation + , ciuId :: CryptoUUIDUser + } deriving (Eq,Ord) + +deriveJSON defaultOptions + { fieldLabelModifier = camelToPathPiece' 1 + } ''CorrectInterfaceUser + + +data CorrectInterfaceResponse + = CorrectInterfaceResponseSuccess + { cirsUser :: CorrectInterfaceUser + , cirsResults :: Map ExamPartNumber (Maybe ExamResultPoints) + , cirsGrade :: Maybe (Maybe ExamResultPassedGrade) + , cirsTime :: UTCTime + } + | CorrectInterfaceResponseAmbiguous + { ciraUsers :: Set CorrectInterfaceUser + , ciraMessage :: Text + } + | CorrectInterfaceResponseFailure + { cirfUser :: Maybe CorrectInterfaceUser + , cirfMessage :: Text + } + | CorrectInterfaceResponseNoOp + { cirnUsers :: Set CorrectInterfaceUser + } + +deriveToJSON defaultOptions + { constructorTagModifier = camelToPathPiece' 3 + , fieldLabelModifier = camelToPathPiece' 1 + , sumEncoding = TaggedObject "status" "results" + , omitNothingFields = True + } ''CorrectInterfaceResponse + + +data CorrectInterfaceRequest + = CorrectInterfaceRequest + { ciqUser :: Either Text (CryptoID UUID (Key User)) + , ciqResults :: Maybe (NonNull (Map ExamPartNumber (Maybe Points))) + , ciqGrade :: Maybe (Maybe ExamResultPassedGrade) + } + +instance FromJSON CorrectInterfaceRequest where + parseJSON = JSON.withObject "CorrectInterfaceRequest" $ \o -> do + ciqUser <- Right <$> o JSON..: "user" <|> Left <$> o JSON..: "user" + results <- o JSON..:? "results" + ciqResults <- for results $ maybe (fail "Results may not be nullable") return . fromNullable + ciqGrade <- if + | "grade" `HashMap.member` o + -> Just <$> o JSON..: "grade" + | otherwise + -> pure Nothing + return CorrectInterfaceRequest{..} From be2eb3c38d6539056456978b37c47c049d1cd683 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel <> Date: Tue, 11 Aug 2020 16:01:51 +0200 Subject: [PATCH 02/31] feat(eecorrectr): add handlers and navigation --- messages/uniworx/de-de-formal.msg | 2 ++ messages/uniworx/en-eu.msg | 2 ++ routes | 1 + src/Foundation.hs | 36 ++++++++++++++++++++++++++++- src/Handler/ExternalExam.hs | 1 + src/Handler/ExternalExam/Correct.hs | 13 +++++++++++ 6 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/Handler/ExternalExam/Correct.hs diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index c189e8b5f..0f9ef0772 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -1346,6 +1346,7 @@ MenuExternalExamUsers: Teilnehmer MenuExternalExamEdit: Bearbeiten MenuExternalExamNew: Neue externe Prüfung MenuExternalExamList: Externe Prüfungen +MenuExternalExamCorrect: Prüfungsleistungen eintragen MenuParticipantsList: Kursteilnehmerlisten MenuParticipantsIntersect: Überschneidung von Kursteilnehmern MenuAllocationUsers: Bewerber @@ -1417,6 +1418,7 @@ BreadcrumbExternalExamEdit: Editieren BreadcrumbExternalExamUsers: Teilnehmer BreadcrumbExternalExamGrades: Prüfungsleistungen BreadcrumbExternalExamStaffInvite: Einladung zum Prüfer +BreadcrumbExternalExamCorrect: Prüfungsleistungen eintragen BreadcrumbParticipantsList: Kursteilnehmerlisten BreadcrumbParticipants: Kursteilnehmerliste BreadcrumbExamAutoOccurrence: Automatische Termin-/Raumverteilung diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index 0c443642b..0a790037f 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -1346,6 +1346,7 @@ MenuExternalExamUsers: Participants MenuExternalExamEdit: Edit MenuExternalExamNew: New external exam MenuExternalExamList: External exams +MenuExternalExamCorrect: Enter exam results MenuParticipantsList: Lists of course participants MenuParticipantsIntersect: Common course participants MenuAllocationUsers: Applicants @@ -1417,6 +1418,7 @@ BreadcrumbExternalExamEdit: Edit BreadcrumbExternalExamUsers: Participants BreadcrumbExternalExamGrades: Exam results BreadcrumbExternalExamStaffInvite: Invitation +BreadcrumbExternalExamCorrect: Enter exam results BreadcrumbParticipantsList: Lists of course participants BreadcrumbParticipants: Course participants BreadcrumbExamAutoOccurrence: Automatic occurrence/room distribution diff --git a/routes b/routes index 0b790ae36..1bb1c3f9a 100644 --- a/routes +++ b/routes @@ -92,6 +92,7 @@ /users EEUsersR GET POST /grades EEGradesR GET POST !exam-office /staff-invite EEStaffInviteR GET POST + /correct EECorrectR GET POST /term TermShowR GET !free diff --git a/src/Foundation.hs b/src/Foundation.hs index 1d8cd15b4..f999ac9db 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -2696,7 +2696,8 @@ instance YesodBreadcrumbs UniWorX where EEEditR -> i18nCrumb MsgBreadcrumbExternalExamEdit . Just $ EExamR tid ssh coursen examn EEShowR EEUsersR -> i18nCrumb MsgBreadcrumbExternalExamUsers . Just $ EExamR tid ssh coursen examn EEShowR EEGradesR -> i18nCrumb MsgBreadcrumbExternalExamGrades . Just $ EExamR tid ssh coursen examn EEShowR - EEStaffInviteR -> i18nCrumb MsgBreadcrumbExternalExamStaffInvite . Just $ EExamR tid ssh coursen examn EEShowR + EEStaffInviteR -> i18nCrumb MsgBreadcrumbExternalExamStaffInvite . Just $ EExamR tid ssh coursen examn EEShowR + EECorrectR -> i18nCrumb MsgBreadcrumbExternalExamCorrect . Just $ EExamR tid ssh coursen examn EEShowR -- breadcrumb _ = return ("Uni2work", Nothing) -- Default is no breadcrumb at all @@ -4413,9 +4414,31 @@ pageActions (EExamR tid ssh coursen examn EEShowR) = return } , navChildren = [] } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExternalExamCorrect + , navRoute = EExamR tid ssh coursen examn EECorrectR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } ] pageActions (EExamR tid ssh coursen examn EEGradesR) = return [ NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExternalExamCorrect + , navRoute = EExamR tid ssh coursen examn EECorrectR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } + , NavPageActionPrimary { navLink = NavLink { navLabel = MsgMenuExternalExamUsers , navRoute = EExamR tid ssh coursen examn EEUsersR @@ -4450,6 +4473,17 @@ pageActions (EExamR tid ssh coursen examn EEUsersR) = return } , navChildren = [] } + , NavPageActionPrimary + { navLink = NavLink + { navLabel = MsgMenuExternalExamCorrect + , navRoute = EExamR tid ssh coursen examn EECorrectR + , navAccess' = return True + , navType = NavTypeLink { navModal = False } + , navQuick' = mempty + , navForceActive = False + } + , navChildren = [] + } , NavPageActionPrimary { navLink = NavLink { navLabel = MsgMenuExternalExamEdit diff --git a/src/Handler/ExternalExam.hs b/src/Handler/ExternalExam.hs index ac53b0246..fe4c662f3 100644 --- a/src/Handler/ExternalExam.hs +++ b/src/Handler/ExternalExam.hs @@ -8,3 +8,4 @@ import Handler.ExternalExam.Show as Handler.ExternalExam import Handler.ExternalExam.Edit as Handler.ExternalExam import Handler.ExternalExam.Users as Handler.ExternalExam import Handler.ExternalExam.StaffInvite as Handler.ExternalExam +import Handler.ExternalExam.Correct as Handler.ExternalExam diff --git a/src/Handler/ExternalExam/Correct.hs b/src/Handler/ExternalExam/Correct.hs new file mode 100644 index 000000000..e930a0680 --- /dev/null +++ b/src/Handler/ExternalExam/Correct.hs @@ -0,0 +1,13 @@ +module Handler.ExternalExam.Correct + ( getEECorrectR, postEECorrectR + ) where + +import Import + + +getEECorrectR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html +getEECorrectR _tid _ssh _coursen _examn = error "getEECorrectR WIP" + + +postEECorrectR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Void +postEECorrectR _tid _ssh _coursen _examn = error "postEECorrectR WIP" From de02895ed0ddc7ed119b76fded1d3eaec24448ba Mon Sep 17 00:00:00 2001 From: Sarah Vaupel <> Date: Tue, 11 Aug 2020 21:18:59 +0200 Subject: [PATCH 03/31] feat(eecorrectr): basic handler structure (WIP) --- messages/uniworx/de-de-formal.msg | 1 + messages/uniworx/en-eu.msg | 1 + src/Handler/ExternalExam/Correct.hs | 151 +++++++++++++++++- src/Handler/Utils/ExternalExam.hs | 15 ++ .../de-de-formal.hamlet | 30 ++++ .../en-eu.hamlet | 27 ++++ 6 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 src/Handler/Utils/ExternalExam.hs create mode 100644 templates/i18n/external-exam-correct-explanation/de-de-formal.hamlet create mode 100644 templates/i18n/external-exam-correct-explanation/en-eu.hamlet diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 0f9ef0772..0850a4677 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -1435,6 +1435,7 @@ BreadcrumbAdminCrontab: Crontab ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn} ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn} +ExternalExamCorrectHeading coursen@CourseName examn@ExamName: Prüfungsleistungen für #{coursen}, #{examn} eintragen ExternalExamUsers coursen@CourseName examn@ExamName: Teilnehmer: #{coursen}, #{examn} TitleMetrics: Metriken diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index 0a790037f..78c59e0e7 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -1435,6 +1435,7 @@ BreadcrumbAdminCrontab: Crontab ExternalExamEdit coursen examn: Edit: #{coursen}, #{examn} ExternalExamGrades coursen examn: Exam achievements: #{coursen}, #{examn} +ExternalExamCorrectHeading coursen examn: Enter exam results for #{coursen}, #{examn} ExternalExamUsers coursen examn: Exam participants: #{coursen}, #{examn} TitleMetrics: Metrics diff --git a/src/Handler/ExternalExam/Correct.hs b/src/Handler/ExternalExam/Correct.hs index e930a0680..401890fc9 100644 --- a/src/Handler/ExternalExam/Correct.hs +++ b/src/Handler/ExternalExam/Correct.hs @@ -4,10 +4,157 @@ module Handler.ExternalExam.Correct import Import +import qualified Data.Set as Set + +import qualified Database.Esqueleto as E +import qualified Database.Esqueleto.Utils as E +import Database.Persist.Sql (transactionUndo) + +import Handler.Utils +import Handler.Utils.ExternalExam + +import Utils.Exam + getEECorrectR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html -getEECorrectR _tid _ssh _coursen _examn = error "getEECorrectR WIP" +getEECorrectR tid ssh coursen examn = do + MsgRenderer mr <- getMsgRenderer + + Entity _eeId ExternalExam{..} <- runDB $ fetchExternalExam tid ssh coursen examn + + mayEditResults <- hasWriteAccessTo $ EExamR tid ssh coursen examn EEUsersR + + let + heading = mr $ MsgExternalExamCorrectHeading coursen examn + + ptsInput :: ExamPartNumber -> Widget + ptsInput = const mempty + + examParts :: [ExamPart] + examParts = [] + + examGrades :: [ExamGrade] + examGrades = universeF + + examGradingMode = externalExamGradingMode + + examCorrectIdent :: Text + examCorrectIdent = "TODO" -- TODO fix below + + -- examCorrectIdent <- encrypt eeId :: Handler (CryptoID UUID ExternalExamId) + + siteLayoutMsg heading $ do + setTitleI heading + let + examCorrectExplanation = $(i18nWidgetFile "external-exam-correct-explanation") + $(widgetFile "exam-correct") postEECorrectR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Void -postEECorrectR _tid _ssh _coursen _examn = error "postEECorrectR WIP" +postEECorrectR tid ssh coursen examn = do + MsgRenderer mr <- getMsgRenderer + + CorrectInterfaceRequest{..} <- requireCheckJsonBody + + mayEditResults <- hasWriteAccessTo $ EExamR tid ssh coursen examn EEUsersR + + response <- runDB . exceptT (<$ transactionUndo) return $ do + Entity eeId ExternalExam{..} <- lift $ fetchExternalExam tid ssh coursen examn + euid <- traverse decrypt ciqUser + + guardMExceptT (maybe True ((>= 3) . length) $ euid ^? _Left) $ -- TODO rethink max needle length + CorrectInterfaceResponseFailure Nothing <$> (getMessageRender <*> pure MsgExamCorrectErrorNeedleTooShort) + + matches <- lift . E.select . E.from $ \user -> do + let mUserIdent = euid ^? _Left + E.where_ $ either (const E.false) (\uid -> user E.^. UserId E.==. E.val uid) euid + E.||. (case mUserIdent of + Just userIdent -> + user E.^. UserSurname E.==. E.val userIdent + E.||. user E.^. UserSurname `E.hasInfix` E.val userIdent + E.||. user E.^. UserFirstName E.==. E.val userIdent + E.||. user E.^. UserFirstName `E.hasInfix` E.val userIdent + E.||. user E.^. UserDisplayName E.==. E.val userIdent + E.||. user E.^. UserDisplayName `E.hasInfix` E.val userIdent + E.||. user E.^. UserMatrikelnummer E.==. E.val mUserIdent + E.||. user E.^. UserMatrikelnummer `E.hasInfix` E.val mUserIdent + Nothing -> E.false) + return user + + let + userToResponse (Entity uid User{..}) = do -- TODO move to util + uuid <- encrypt uid + return CorrectInterfaceUser + { ciuSurname = userSurname + , ciuDisplayName = userDisplayName + , ciuMatNr = userMatrikelnummer + , ciuId = uuid + } + + if + | is _Nothing ciqResults, is _Nothing ciqGrade -> do + users <- traverse userToResponse matches + return CorrectInterfaceResponseNoOp + { cirnUsers = Set.fromList users + } + | [match@(Entity uid _)] <- matches -> do + now <- liftIO getCurrentTime + newExamResult <- for ciqGrade $ \ciqGrade' -> lift $ do + unless mayEditResults $ + permissionDeniedI MsgUnauthorizedExamCorrectorGrade + mOldResult <- getBy $ UniqueExternalExamResult eeId uid + if + | Just (Entity oldId _) <- mOldResult, is _Nothing ciqGrade' -> do + delete oldId + audit $ TransactionExternalExamResultDelete eeId uid + return Nothing + | Just resultGrade <- ciqGrade' -> let + mOld = externalExamResultResult . entityVal <$> mOldResult + in if + | ciqGrade' /= mOld -> do + newResult <- upsert ExternalExamResult + { externalExamResultExam = eeId + , externalExamResultUser = uid + , externalExamResultResult = resultGrade + , externalExamResultTime = now -- TODO add and use utcTimeField + , externalExamResultLastChanged = now + } + [ ExternalExamResultResult =. resultGrade + , ExternalExamResultTime =. now -- TODO add and use utcTimeField + , ExternalExamResultLastChanged =. now + ] + audit $ TransactionExternalExamResultEdit eeId uid + return $ newResult ^? _entityVal . _externalExamResultResult + | otherwise -> return $ mOldResult ^? _Just . _entityVal . _externalExamResultResult + | otherwise -> return Nothing + + user <- userToResponse match + return CorrectInterfaceResponseSuccess + { cirsUser = user + , cirsResults = mempty + , cirsGrade = newExamResult + , cirsTime = now + } + + | [] <- matches -> return CorrectInterfaceResponseFailure + { cirfUser = Nothing + , cirfMessage = mr MsgExamCorrectErrorNoMatchingParticipants -- TODO use new msg + } + + | otherwise -> do + users <- traverse userToResponse matches + return CorrectInterfaceResponseAmbiguous + { ciraUsers = Set.fromList users + , ciraMessage = mr MsgExamCorrectErrorMultipleMatchingParticipants -- TODO use new msg + } + + let + responseStatus = case response of + CorrectInterfaceResponseSuccess{} -> ok200 + CorrectInterfaceResponseNoOp{} -> ok200 + _ -> badRequest400 + + whenM acceptsJson $ + sendResponseStatus responseStatus $ toJSON response + + redirect $ EExamR tid ssh coursen examn EEShowR diff --git a/src/Handler/Utils/ExternalExam.hs b/src/Handler/Utils/ExternalExam.hs new file mode 100644 index 000000000..1d411e960 --- /dev/null +++ b/src/Handler/Utils/ExternalExam.hs @@ -0,0 +1,15 @@ +module Handler.Utils.ExternalExam + ( fetchExternalExam + ) where + +import Import + + +fetchExternalExam :: MonadHandler m => TermId -> SchoolId -> CourseName -> ExamName -> ReaderT SqlBackend m (Entity ExternalExam) +fetchExternalExam tid ssh coursen examn = + let cachId = encodeUtf8 $ tshow (tid, ssh, coursen, examn) + in cachedBy cachId $ do + mExtEx <- getBy $ UniqueExternalExam tid ssh coursen examn + case mExtEx of + Just extEx -> return extEx + _ -> notFound diff --git a/templates/i18n/external-exam-correct-explanation/de-de-formal.hamlet b/templates/i18n/external-exam-correct-explanation/de-de-formal.hamlet new file mode 100644 index 000000000..5cc48c036 --- /dev/null +++ b/templates/i18n/external-exam-correct-explanation/de-de-formal.hamlet @@ -0,0 +1,30 @@ +$newline never +

+ Um eine Prüfungsleistung einzutragen können Sie in der # + Teilnehmer-Spalte einen beliebigen eindeutigen Identifikator des # + Teilnehmers angeben.
+ + Es können nur Ergebnisse für Studierende eingetragen werden, die # + bereits Prüfungsteilnehmer sind. # + Über diese Oberfläche können keine neuen Benutzer zur Klausur # + angemeldet werden.
+ + Vermutlich eindeutig ist die Matrikelnummer des Teilnehmers, aber # + auch der Name oder ein Teil der Matrikelnummer können unter # + Umständen bereits eindeutig sein.
+ + Wenn Felder für Ergebnisse frei gelassen werden, wird an dieser # + Stelle nichts in die Datenbank eingetragen.
+ + Beim Senden von Ergebnissen wird der bisherige Stand in der # + Datenbank überschrieben. # + Es werden auch Ergebnisse überschrieben, die andere Benutzer # + eingetragen haben.
+ + Bereits eingetragene Ergebnisse können auch gelöscht werden; es ist # + danach für den jeweiligen Teil der Prüfung kein Ergebnis mehr in der # + Datenbank hinterlegt.
+ + Falls eine automatische Notenberechnung konfiguriert ist, müssen die # + berechneten Ergebnisse noch auf der Seite der Klausurteilnehmerliste # + akzeptiert werden. diff --git a/templates/i18n/external-exam-correct-explanation/en-eu.hamlet b/templates/i18n/external-exam-correct-explanation/en-eu.hamlet new file mode 100644 index 000000000..2b41a9ee8 --- /dev/null +++ b/templates/i18n/external-exam-correct-explanation/en-eu.hamlet @@ -0,0 +1,27 @@ +$newline never +

+ To enter a participant's exam achievement you can submit any string # + that uniquely identifies the participant.
+ + Results can only be entered for users who are already exam # + participants. # + No new participants can be added to the exam using this interface. # + + Matriculation numbers are likely unique. # + The participant's name or a part of their matriculation number may # + also be sufficiently unique.
+ + If any fields are left blank no result is saved for that part of the # + exam.
+ + When entering results, the current state in the database is # + overwritten. # + Results entered by other users are also overwritten.
+ + It is possible to delete results. # + After doing so no result is saved for that part of the exam within # + the database.
+ + If grades are to be computed automatically for this exam, the # + results need to be accepted. # + This is done via the listing of exam participants. From 33691556abd40041a1707e3f902cfcf5d513342d Mon Sep 17 00:00:00 2001 From: Sarah Vaupel <> Date: Wed, 12 Aug 2020 11:58:15 +0200 Subject: [PATCH 04/31] fix(eecorrectr): use default time --- src/Handler/ExternalExam/Correct.hs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Handler/ExternalExam/Correct.hs b/src/Handler/ExternalExam/Correct.hs index 401890fc9..a4472ea91 100644 --- a/src/Handler/ExternalExam/Correct.hs +++ b/src/Handler/ExternalExam/Correct.hs @@ -112,15 +112,16 @@ postEECorrectR tid ssh coursen examn = do mOld = externalExamResultResult . entityVal <$> mOldResult in if | ciqGrade' /= mOld -> do + let resultTime = maybe now id externalExamDefaultTime -- TODO add option to override default? newResult <- upsert ExternalExamResult { externalExamResultExam = eeId , externalExamResultUser = uid , externalExamResultResult = resultGrade - , externalExamResultTime = now -- TODO add and use utcTimeField + , externalExamResultTime = resultTime , externalExamResultLastChanged = now } [ ExternalExamResultResult =. resultGrade - , ExternalExamResultTime =. now -- TODO add and use utcTimeField + , ExternalExamResultTime =. resultTime , ExternalExamResultLastChanged =. now ] audit $ TransactionExternalExamResultEdit eeId uid From 36d45fcc0eab38792db6af1bbfedaf677abf48eb Mon Sep 17 00:00:00 2001 From: Sarah Vaupel <> Date: Wed, 12 Aug 2020 13:35:06 +0200 Subject: [PATCH 05/31] refactor(exam-correct): move userToResponse to utils --- src/Handler/Exam/Correct.hs | 10 ---------- src/Handler/ExternalExam/Correct.hs | 10 ---------- src/Utils/Exam.hs | 12 +++++++++++- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/Handler/Exam/Correct.hs b/src/Handler/Exam/Correct.hs index c74970da7..d03af8000 100644 --- a/src/Handler/Exam/Correct.hs +++ b/src/Handler/Exam/Correct.hs @@ -83,16 +83,6 @@ postECorrectR tid ssh csh examn = do Nothing -> E.val False) return user - let - userToResponse (Entity uid User{..}) = do - uuid <- encrypt uid - return CorrectInterfaceUser - { ciuSurname = userSurname - , ciuDisplayName = userDisplayName - , ciuMatNr = userMatrikelnummer - , ciuId = uuid - } - if -- on no-op request, answer with 200 and a set of all participant matches | is _Nothing ciqResults, is _Nothing ciqGrade -> do diff --git a/src/Handler/ExternalExam/Correct.hs b/src/Handler/ExternalExam/Correct.hs index a4472ea91..319b78318 100644 --- a/src/Handler/ExternalExam/Correct.hs +++ b/src/Handler/ExternalExam/Correct.hs @@ -81,16 +81,6 @@ postEECorrectR tid ssh coursen examn = do Nothing -> E.false) return user - let - userToResponse (Entity uid User{..}) = do -- TODO move to util - uuid <- encrypt uid - return CorrectInterfaceUser - { ciuSurname = userSurname - , ciuDisplayName = userDisplayName - , ciuMatNr = userMatrikelnummer - , ciuId = uuid - } - if | is _Nothing ciqResults, is _Nothing ciqGrade -> do users <- traverse userToResponse matches diff --git a/src/Utils/Exam.hs b/src/Utils/Exam.hs index bca2727ca..40dd0f18d 100644 --- a/src/Utils/Exam.hs +++ b/src/Utils/Exam.hs @@ -1,7 +1,7 @@ module Utils.Exam ( CorrectInterfaceRequest(..) , CorrectInterfaceResponse(..) - , CorrectInterfaceUser(..) + , CorrectInterfaceUser(..), userToResponse ) where import Import.NoFoundation @@ -22,6 +22,16 @@ deriveJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 } ''CorrectInterfaceUser +userToResponse :: (MonadHandler m, MonadCrypto m, MonadCryptoKey m ~ CryptoIDKey) => Entity User -> m CorrectInterfaceUser +userToResponse (Entity uid User{..}) = do + uuid <- encrypt uid + return CorrectInterfaceUser + { ciuSurname = userSurname + , ciuDisplayName = userDisplayName + , ciuMatNr = userMatrikelnummer + , ciuId = uuid + } + data CorrectInterfaceResponse = CorrectInterfaceResponseSuccess From 4515987446c727505639018b57fb20680b156363 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel <> Date: Wed, 12 Aug 2020 13:51:38 +0200 Subject: [PATCH 06/31] refactor(exam-correct): move responseStatus to utils --- src/Handler/Exam/Correct.hs | 8 +------- src/Handler/ExternalExam/Correct.hs | 8 +------- src/Utils/Exam.hs | 7 ++++++- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/Handler/Exam/Correct.hs b/src/Handler/Exam/Correct.hs index d03af8000..772fcef85 100644 --- a/src/Handler/Exam/Correct.hs +++ b/src/Handler/Exam/Correct.hs @@ -186,13 +186,7 @@ postECorrectR tid ssh csh examn = do , ciraUsers = Set.fromList users } - let - responseStatus = case response of - CorrectInterfaceResponseSuccess{} -> ok200 - CorrectInterfaceResponseNoOp{} -> ok200 - _ -> badRequest400 - whenM acceptsJson $ - sendResponseStatus responseStatus $ toJSON response + sendResponseStatus (ciResponseStatus response) $ toJSON response redirect $ CExamR tid ssh csh examn EShowR diff --git a/src/Handler/ExternalExam/Correct.hs b/src/Handler/ExternalExam/Correct.hs index 319b78318..f7e3d9270 100644 --- a/src/Handler/ExternalExam/Correct.hs +++ b/src/Handler/ExternalExam/Correct.hs @@ -139,13 +139,7 @@ postEECorrectR tid ssh coursen examn = do , ciraMessage = mr MsgExamCorrectErrorMultipleMatchingParticipants -- TODO use new msg } - let - responseStatus = case response of - CorrectInterfaceResponseSuccess{} -> ok200 - CorrectInterfaceResponseNoOp{} -> ok200 - _ -> badRequest400 - whenM acceptsJson $ - sendResponseStatus responseStatus $ toJSON response + sendResponseStatus (ciResponseStatus response) $ toJSON response redirect $ EExamR tid ssh coursen examn EEShowR diff --git a/src/Utils/Exam.hs b/src/Utils/Exam.hs index 40dd0f18d..c7847d6f5 100644 --- a/src/Utils/Exam.hs +++ b/src/Utils/Exam.hs @@ -1,6 +1,6 @@ module Utils.Exam ( CorrectInterfaceRequest(..) - , CorrectInterfaceResponse(..) + , CorrectInterfaceResponse(..), ciResponseStatus , CorrectInterfaceUser(..), userToResponse ) where @@ -59,6 +59,11 @@ deriveToJSON defaultOptions , omitNothingFields = True } ''CorrectInterfaceResponse +ciResponseStatus :: CorrectInterfaceResponse -> Status +ciResponseStatus CorrectInterfaceResponseSuccess{} = ok200 +ciResponseStatus CorrectInterfaceResponseNoOp{} = ok200 +ciResponseStatus _ = badRequest400 + data CorrectInterfaceRequest = CorrectInterfaceRequest From 5d9ca454fa5353009c33678e21d9f49bd45f6cc3 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel <> Date: Wed, 12 Aug 2020 14:31:55 +0200 Subject: [PATCH 07/31] fix(eecorrectr): encrypt eeid --- src/CryptoID.hs | 1 + src/Handler/ExternalExam/Correct.hs | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/CryptoID.hs b/src/CryptoID.hs index 17b75dc85..a53d59d57 100644 --- a/src/CryptoID.hs +++ b/src/CryptoID.hs @@ -72,6 +72,7 @@ decCryptoIDs [ ''SubmissionId , ''CourseNewsId , ''CourseEventId , ''TutorialId + , ''ExternalExamId ] decCryptoIDKeySize diff --git a/src/Handler/ExternalExam/Correct.hs b/src/Handler/ExternalExam/Correct.hs index f7e3d9270..349dd6154 100644 --- a/src/Handler/ExternalExam/Correct.hs +++ b/src/Handler/ExternalExam/Correct.hs @@ -20,7 +20,7 @@ getEECorrectR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html getEECorrectR tid ssh coursen examn = do MsgRenderer mr <- getMsgRenderer - Entity _eeId ExternalExam{..} <- runDB $ fetchExternalExam tid ssh coursen examn + Entity eeId ExternalExam{..} <- runDB $ fetchExternalExam tid ssh coursen examn mayEditResults <- hasWriteAccessTo $ EExamR tid ssh coursen examn EEUsersR @@ -38,10 +38,7 @@ getEECorrectR tid ssh coursen examn = do examGradingMode = externalExamGradingMode - examCorrectIdent :: Text - examCorrectIdent = "TODO" -- TODO fix below - - -- examCorrectIdent <- encrypt eeId :: Handler (CryptoID UUID ExternalExamId) + examCorrectIdent <- encrypt eeId :: Handler CryptoUUIDExternalExam siteLayoutMsg heading $ do setTitleI heading From e6d540e55ca14fde84acd9770b0d7ceaa78ee945 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel <> Date: Wed, 12 Aug 2020 14:39:13 +0200 Subject: [PATCH 08/31] chore(eecorrectr): update explanation --- .../de-de-formal.hamlet | 23 ++++--------------- .../en-eu.hamlet | 20 ++++------------ 2 files changed, 9 insertions(+), 34 deletions(-) diff --git a/templates/i18n/external-exam-correct-explanation/de-de-formal.hamlet b/templates/i18n/external-exam-correct-explanation/de-de-formal.hamlet index 5cc48c036..b10542587 100644 --- a/templates/i18n/external-exam-correct-explanation/de-de-formal.hamlet +++ b/templates/i18n/external-exam-correct-explanation/de-de-formal.hamlet @@ -4,27 +4,14 @@ $newline never Teilnehmer-Spalte einen beliebigen eindeutigen Identifikator des # Teilnehmers angeben.
- Es können nur Ergebnisse für Studierende eingetragen werden, die # - bereits Prüfungsteilnehmer sind. # - Über diese Oberfläche können keine neuen Benutzer zur Klausur # - angemeldet werden.
- Vermutlich eindeutig ist die Matrikelnummer des Teilnehmers, aber # - auch der Name oder ein Teil der Matrikelnummer können unter # + auch der (volle) Name oder ein Teil der Matrikelnummer können unter # Umständen bereits eindeutig sein.
- Wenn Felder für Ergebnisse frei gelassen werden, wird an dieser # - Stelle nichts in die Datenbank eingetragen.
- - Beim Senden von Ergebnissen wird der bisherige Stand in der # + Beim Senden von Prüfungsleistungen wird der bisherige Stand in der # Datenbank überschrieben. # - Es werden auch Ergebnisse überschrieben, die andere Benutzer # + Es werden auch Prüfungsleistungen überschrieben, die andere Benutzer # eingetragen haben.
- Bereits eingetragene Ergebnisse können auch gelöscht werden; es ist # - danach für den jeweiligen Teil der Prüfung kein Ergebnis mehr in der # - Datenbank hinterlegt.
- - Falls eine automatische Notenberechnung konfiguriert ist, müssen die # - berechneten Ergebnisse noch auf der Seite der Klausurteilnehmerliste # - akzeptiert werden. + Bereits eingetragene Prüfungsleistungen können auch gelöscht werden; # + es ist danach keine Prüfungsleistung mehr in der Datenbank hinterlegt. diff --git a/templates/i18n/external-exam-correct-explanation/en-eu.hamlet b/templates/i18n/external-exam-correct-explanation/en-eu.hamlet index 2b41a9ee8..d46d91541 100644 --- a/templates/i18n/external-exam-correct-explanation/en-eu.hamlet +++ b/templates/i18n/external-exam-correct-explanation/en-eu.hamlet @@ -1,27 +1,15 @@ $newline never

- To enter a participant's exam achievement you can submit any string # + To enter a participant's exam result you can submit any string # that uniquely identifies the participant.
- Results can only be entered for users who are already exam # - participants. # - No new participants can be added to the exam using this interface. # - Matriculation numbers are likely unique. # - The participant's name or a part of their matriculation number may # - also be sufficiently unique.
- - If any fields are left blank no result is saved for that part of the # - exam.
+ The participant's (full) name or a part of their matriculation number # + may also be sufficiently unique.
When entering results, the current state in the database is # overwritten. # Results entered by other users are also overwritten.
It is possible to delete results. # - After doing so no result is saved for that part of the exam within # - the database.
- - If grades are to be computed automatically for this exam, the # - results need to be accepted. # - This is done via the listing of exam participants. + After doing so no exam result is saved within the database. From 62fef3547549f6a04203f405e0f1fcc4cb58109d Mon Sep 17 00:00:00 2001 From: Sarah Vaupel <> Date: Wed, 12 Aug 2020 14:49:44 +0200 Subject: [PATCH 09/31] refactor(exam-correct): get rid of hardcoded minNeedleLength --- src/Handler/Exam/Correct.hs | 9 +++++++-- src/Handler/ExternalExam/Correct.hs | 6 +++++- templates/exam-correct.hamlet | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Handler/Exam/Correct.hs b/src/Handler/Exam/Correct.hs index 772fcef85..fbd89ff81 100644 --- a/src/Handler/Exam/Correct.hs +++ b/src/Handler/Exam/Correct.hs @@ -17,6 +17,11 @@ import Handler.Utils.Exam (fetchExam) import Utils.Exam +-- | Minimum length of a participant identifier. Identifiers that are shorter would result in too many query results and are therefor rejected. +minNeedleLength :: Int +minNeedleLength = 3 + + getECorrectR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getECorrectR tid ssh csh examn = do MsgRenderer mr <- getMsgRenderer @@ -54,12 +59,12 @@ postECorrectR tid ssh csh examn = do CorrectInterfaceRequest{..} <- requireCheckJsonBody mayEditResults <- hasWriteAccessTo $ CExamR tid ssh csh examn EUsersR - + response <- runDB . exceptT (<$ transactionUndo) return $ do Entity eId Exam{..} <- lift $ fetchExam tid ssh csh examn euid <- traverse decrypt ciqUser - guardMExceptT (maybe True ((>= 3) . length) $ euid ^? _Left) $ + guardMExceptT (maybe True ((>= minNeedleLength) . length) $ euid ^? _Left) $ CorrectInterfaceResponseFailure Nothing <$> (getMessageRender <*> pure MsgExamCorrectErrorNeedleTooShort) diff --git a/src/Handler/ExternalExam/Correct.hs b/src/Handler/ExternalExam/Correct.hs index 349dd6154..18583e385 100644 --- a/src/Handler/ExternalExam/Correct.hs +++ b/src/Handler/ExternalExam/Correct.hs @@ -15,6 +15,10 @@ import Handler.Utils.ExternalExam import Utils.Exam +-- | Minimum length of a participant identifier. Identifiers that are shorter would result in too many query results and are therefor rejected. +minNeedleLength :: Int +minNeedleLength = 3 -- TODO rethink + getEECorrectR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html getEECorrectR tid ssh coursen examn = do @@ -59,7 +63,7 @@ postEECorrectR tid ssh coursen examn = do Entity eeId ExternalExam{..} <- lift $ fetchExternalExam tid ssh coursen examn euid <- traverse decrypt ciqUser - guardMExceptT (maybe True ((>= 3) . length) $ euid ^? _Left) $ -- TODO rethink max needle length + guardMExceptT (maybe True ((>= minNeedleLength) . length) $ euid ^? _Left) $ CorrectInterfaceResponseFailure Nothing <$> (getMessageRender <*> pure MsgExamCorrectErrorNeedleTooShort) matches <- lift . E.select . E.from $ \user -> do diff --git a/templates/exam-correct.hamlet b/templates/exam-correct.hamlet index 47ab48deb..309889bc1 100644 --- a/templates/exam-correct.hamlet +++ b/templates/exam-correct.hamlet @@ -23,7 +23,7 @@ $newline never - +