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