From 2cf4895231e889711dc62896937e775b3d85fe79 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Mon, 2 Mar 2020 17:21:07 +0100 Subject: [PATCH 001/720] feat(external-exams): add actions to EEUsers --- messages/uniworx/de-de-formal.msg | 5 ++++- messages/uniworx/en-eu.msg | 5 ++++- src/Handler/ExamOffice/ExternalExam.hs | 3 +++ src/Handler/Utils/ExternalExam/Users.hs | 15 ++++++++++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 39b865efb..0baaa428f 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -1729,6 +1729,9 @@ ExamUserSyncLastChange: Zuletzt geändert ExamUserMarkSynchronised: Prüfungsleistung als synchronisiert markieren ExternalExamUserMarkSynchronised: Prüfungsleistung als synchronisiert markieren ExternalExamUserMarkSynchronisedTip: Sollen beim CSV-Export automatisch alle heruntergeladenen Prüfungsleistungen als synchronisiert markiert werden? Diese Markierung dient als Hinweis an andere Prüfungsbeauftragte und die Kursverwalter, dass die Leistung an der korrekten Stelle vermerkt wurde und keiner weiteren Handlung bedarf. +ExternalExamUserEditOccurrence: Termin ändern +ExternalExamUserEditResult: Prüfungsergebnis ändern +ExternalExamUserDelete: Prüfungsteilnehmer entfernen ExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert markieren ExamUserMarkSynchronisedCsvTip: Sollen beim CSV-Export automatisch alle heruntergeladenen Prüfungsleistungen als synchronisiert markiert werden? Diese Markierung dient als Hinweis an andere Prüfungsbeauftragte und die Kursverwalter, dass die Leistung an der korrekten Stelle vermerkt wurde und keiner weiteren Handlung bedarf. @@ -2370,4 +2373,4 @@ AllocationPrioritiesTitle tid@TermId ssh@SchoolId ash@AllocationShorthand: #{tid AllocationPrioritiesFile: CSV-Datei AllocationPrioritiesSunk num@Int64: Zentrale Prioritäten für #{num} Bewerber erfolgreich hinterlegt AllocationPrioritiesMissing num@Int64: Für #{num} Bewerber ist keine zentrale Priorität hinterlegt, da in der hochgeladenen CSV-Datei die #{pluralDE num "entsprechende Matrikelnummer" "entsprechenden Matrikelnummern"} nicht gefunden #{pluralDE num "wurde" "wurden"} -AllocationMissingPrioritiesIgnored: Bewerber, für die keine zentrale Priorität angegeben wird, werden bei der Vergabe ignoriert! \ No newline at end of file +AllocationMissingPrioritiesIgnored: Bewerber, für die keine zentrale Priorität angegeben wird, werden bei der Vergabe ignoriert! diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index 224cb684a..3c551257c 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -1721,6 +1721,9 @@ ExamUserSyncLastChange: Last changed ExamUserMarkSynchronised: Mark exam achievements as synchronised ExternalExamUserMarkSynchronised: Mark exam achievements as synchronised ExternalExamUserMarkSynchronisedTip: Should all exam achievements, that are included in the download, be marked as synchronised? Marking exam achievemnts as synchronised serves as a notice to other exam offices and course administrators, that the exam achievement has been dealt with properly such that no further action is required. +ExternalExamUserEditOccurrence: Edit occurrence +ExternalExamUserEditResult: Edit exam result +ExternalExamUserDelete: Delete exam participant ExamUserMarkSynchronisedCsv: Mark exam achievements as synchronised while exporting ExamUserMarkSynchronisedCsvTip: Should all exam achievements, that are included in the download, be marked as synchronised? Marking exam achievemnts as synchronised serves as a notice to other exam offices and course administrators, that the exam achievement has been dealt with properly such that no further action is required. @@ -2335,4 +2338,4 @@ InfoLecturerExams: Exams InfoLecturerAllocations: Central allocations ParticipantsIntersectCourseOption tid@TermId ssh@SchoolId coursen@CourseName: #{tid} - #{ssh} - #{coursen} -ParticipantsIntersectCourses: Courses \ No newline at end of file +ParticipantsIntersectCourses: Courses diff --git a/src/Handler/ExamOffice/ExternalExam.hs b/src/Handler/ExamOffice/ExternalExam.hs index db65d9c8c..c3ae6c290 100644 --- a/src/Handler/ExamOffice/ExternalExam.hs +++ b/src/Handler/ExamOffice/ExternalExam.hs @@ -21,6 +21,9 @@ postEEGradesR tid ssh coursen examn = do return . Just $ do addMessageI Success $ MsgExamUserMarkedSynchronised (length selectedResults) redirect $ EExamR tid ssh coursen examn EEGradesR + (ExternalExamUserEditOccurrenceData _, _) -> error "EditOccurrence not implemented" + (ExternalExamUserEditResultData _, _) -> error "EditResult not implemented" + (ExternalExamUserDeleteData, _) -> error "Delete not implemented" return (usersResult', examUsersTable) diff --git a/src/Handler/Utils/ExternalExam/Users.hs b/src/Handler/Utils/ExternalExam/Users.hs index 545a5bbe2..c3b27b7cd 100644 --- a/src/Handler/Utils/ExternalExam/Users.hs +++ b/src/Handler/Utils/ExternalExam/Users.hs @@ -107,6 +107,9 @@ instance CsvColumnsExplained ExternalExamUserTableCsv where data ExternalExamUserAction = ExternalExamUserMarkSynchronised + | ExternalExamUserEditOccurrence + | ExternalExamUserEditResult + | ExternalExamUserDelete deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) instance Universe ExternalExamUserAction instance Finite ExternalExamUserAction @@ -115,6 +118,9 @@ embedRenderMessage ''UniWorX ''ExternalExamUserAction id data ExternalExamUserActionData = ExternalExamUserMarkSynchronisedData + | ExternalExamUserEditOccurrenceData UTCTime + | ExternalExamUserEditResultData ExamResultPassedGrade + | ExternalExamUserDeleteData newtype ExternalExamUserCsvExportDataGrades = ExternalExamUserCsvExportDataGrades { csvEEUserMarkSynchronised :: Bool @@ -303,7 +309,14 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do (res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf let formRes = (, mempty) . First . Just <$> res return (formRes, formWgt) - _other -> \csrf -> return (FormMissing, toWidget csrf) + EEUMUsers -> \csrf -> do + let + actionMap :: Map ExternalExamUserAction (AForm Handler ExternalExamUserActionData) + actionMap = Map.fromList + [] + (res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf + let formRes = (, mempty) . First . Just <$> res + return (formRes, formWgt) , dbParamsFormEvaluate = liftHandler . runFormPost , dbParamsFormResult = id , dbParamsFormIdent = def From 4d48730abd7b1c4d4d9faab08734289fc6d6afb8 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 3 Mar 2020 09:58:58 +0100 Subject: [PATCH 002/720] feat(eeusersr): stubs for new actions --- messages/uniworx/de-de-formal.msg | 3 +++ messages/uniworx/en-eu.msg | 5 ++++- src/Handler/ExamOffice/ExternalExam.hs | 24 +++++++++++++++++++++--- src/Handler/Utils/ExternalExam/Users.hs | 19 +++++++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 0baaa428f..9992ad69a 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -1739,6 +1739,9 @@ ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfun ExternalExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert markieren ExternalExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfungsleistungen"} als synchronisiert markiert +ExternalExamOccurrenceEdited n@Int: #{n} Termine editiert +ExternalExamResultEdited n@Int: #{n} Prüfungsergebnisse editiert +ExternalExamUserDeleted n@Int: #{n} Prüfungsteilnehmer entfernt ExamOfficeExamUsersHeading: Prüfungsleistungen diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index 3c551257c..d11a41bf9 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -1730,7 +1730,10 @@ ExamUserMarkSynchronisedCsvTip: Should all exam achievements, that are included ExamUserMarkedSynchronised n: Successfully marked #{n} #{pluralEN n "exam achievement" "exam achievements"} as synchronised ExternalExamUserMarkSynchronisedCsv: Mark exam achievements as synchronised while exporting -ExternalExamUserMarkedSynchronised n: Successfully marked #{n} #{pluralEN n "exam achievement" "exam achievements"} as synchronised +ExternalExamUserMarkedSynchronised n: Successfully marked #{n} #{pluralEN n "exam result" "exam results"} as synchronised +ExternalExamOccurrenceEdited n: Successfully edited #{n} #{pluralEN n "occurrence" "occurrences"} +ExternalExamResultEdited n: Successfully edited #{n} #{pluralEN n "exam result" "exam results"} +ExternalExamUserDeleted n: Successfully deleted #{n} #{pluralEN n "exam participant" "exam participants"} ExamOfficeExamUsersHeading: Exam achievements diff --git a/src/Handler/ExamOffice/ExternalExam.hs b/src/Handler/ExamOffice/ExternalExam.hs index c3ae6c290..28fa71e37 100644 --- a/src/Handler/ExamOffice/ExternalExam.hs +++ b/src/Handler/ExamOffice/ExternalExam.hs @@ -21,9 +21,27 @@ postEEGradesR tid ssh coursen examn = do return . Just $ do addMessageI Success $ MsgExamUserMarkedSynchronised (length selectedResults) redirect $ EExamR tid ssh coursen examn EEGradesR - (ExternalExamUserEditOccurrenceData _, _) -> error "EditOccurrence not implemented" - (ExternalExamUserEditResultData _, _) -> error "EditResult not implemented" - (ExternalExamUserDeleteData, _) -> error "Delete not implemented" + + (ExternalExamUserEditOccurrenceData occ, selectedResults) -> do + -- TODO work in progress + forM_ selectedResults $ externalExamEditOccurrence occ + return . Just $ do + addMessageI Success $ MsgExternalExamOccurrenceEdited $ length selectedResults + redirect $ EExamR tid ssh coursen examn EEGradesR + + (ExternalExamUserEditResultData examResult, selectedResults) -> do + -- TODO work in progress + forM_ selectedResults $ externalExamEditResult examResult + return . Just $ do + addMessageI Success $ MsgExternalExamResultEdited $ length selectedResults + redirect $ EExamR tid ssh coursen examn EEGradesR + + (ExternalExamUserDeleteData, selectedResults) -> do + -- TODO work in progress + forM_ selectedResults externalExamDeleteUser + return . Just $ do + addMessageI Success $ MsgExternalExamUserDeleted $ length selectedResults + redirect $ EExamR tid ssh coursen examn EEGradesR return (usersResult', examUsersTable) diff --git a/src/Handler/Utils/ExternalExam/Users.hs b/src/Handler/Utils/ExternalExam/Users.hs index c3b27b7cd..84112e019 100644 --- a/src/Handler/Utils/ExternalExam/Users.hs +++ b/src/Handler/Utils/ExternalExam/Users.hs @@ -482,3 +482,22 @@ externalExamResultMarkSynchronised resId = do } | Entity _ UserFunction{..} <- userFunctions ] + +externalExamEditOccurrence :: UTCTime -> ExternalExamResultId -> DB () +externalExamEditOccurrence _ _ = do + --uid <- requireAuthId + --now <- liftIO getCurrentTime + -- TODO work in progress + return () + +externalExamEditResult :: ExamResultPassedGrade -> ExternalExamResultId -> DB () +externalExamEditResult _ _ = do + --uid <- requireAuthId + --now <- liftIO getCurrentTime + -- TODO work in progress + return () + +externalExamDeleteUser :: ExternalExamResultId -> DB () +externalExamDeleteUser _ = do + -- TODO work in progress + return () From d4b784afba450019d34a7c70eff7992dbad3b9bf Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 3 Mar 2020 15:44:09 +0100 Subject: [PATCH 003/720] feat(eeusersr): more on actions, TODO audit --- messages/uniworx/de-de-formal.msg | 8 +++-- messages/uniworx/en-eu.msg | 8 +++-- src/Handler/ExamOffice/ExternalExam.hs | 40 +++++++++++++++---------- src/Handler/Utils/ExternalExam/Users.hs | 34 +++++++++------------ 4 files changed, 49 insertions(+), 41 deletions(-) diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 9992ad69a..a89735757 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -1737,11 +1737,13 @@ ExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert ExamUserMarkSynchronisedCsvTip: Sollen beim CSV-Export automatisch alle heruntergeladenen Prüfungsleistungen als synchronisiert markiert werden? Diese Markierung dient als Hinweis an andere Prüfungsbeauftragte und die Kursverwalter, dass die Leistung an der korrekten Stelle vermerkt wurde und keiner weiteren Handlung bedarf. ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfungsleistungen"} als synchronisiert markiert +ExternalExamOccurrence: Termin + ExternalExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert markieren ExternalExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfungsleistungen"} als synchronisiert markiert -ExternalExamOccurrenceEdited n@Int: #{n} Termine editiert -ExternalExamResultEdited n@Int: #{n} Prüfungsergebnisse editiert -ExternalExamUserDeleted n@Int: #{n} Prüfungsteilnehmer entfernt +ExternalExamOccurrenceEdited count@Int64: #{count} #{pluralDE count "Termin" "Termine"} editiert +ExternalExamResultEdited count@Int64: #{count} #{pluralDE count "Prüfungsergebnis" "Prüfungsergebnisse"} editiert +ExternalExamUserDeleted count@Int64: #{count} Prüfungsteilnehmer entfernt ExamOfficeExamUsersHeading: Prüfungsleistungen diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index d11a41bf9..06ae2221f 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -1729,11 +1729,13 @@ ExamUserMarkSynchronisedCsv: Mark exam achievements as synchronised while export ExamUserMarkSynchronisedCsvTip: Should all exam achievements, that are included in the download, be marked as synchronised? Marking exam achievemnts as synchronised serves as a notice to other exam offices and course administrators, that the exam achievement has been dealt with properly such that no further action is required. ExamUserMarkedSynchronised n: Successfully marked #{n} #{pluralEN n "exam achievement" "exam achievements"} as synchronised +ExternalExamOccurrence: Occurrence + ExternalExamUserMarkSynchronisedCsv: Mark exam achievements as synchronised while exporting ExternalExamUserMarkedSynchronised n: Successfully marked #{n} #{pluralEN n "exam result" "exam results"} as synchronised -ExternalExamOccurrenceEdited n: Successfully edited #{n} #{pluralEN n "occurrence" "occurrences"} -ExternalExamResultEdited n: Successfully edited #{n} #{pluralEN n "exam result" "exam results"} -ExternalExamUserDeleted n: Successfully deleted #{n} #{pluralEN n "exam participant" "exam participants"} +ExternalExamOccurrenceEdited count: Successfully edited #{count} #{pluralEN count "occurrence" "occurrences"} +ExternalExamResultEdited count: Successfully edited #{count} #{pluralEN count "exam result" "exam results"} +ExternalExamUserDeleted count: Successfully deleted #{count} #{pluralEN count "exam participant" "exam participants"} ExamOfficeExamUsersHeading: Exam achievements diff --git a/src/Handler/ExamOffice/ExternalExam.hs b/src/Handler/ExamOffice/ExternalExam.hs index 28fa71e37..40a040fc8 100644 --- a/src/Handler/ExamOffice/ExternalExam.hs +++ b/src/Handler/ExamOffice/ExternalExam.hs @@ -4,6 +4,9 @@ module Handler.ExamOffice.ExternalExam import Import +import qualified Data.Set as Set +import Database.Persist.Sql (deleteWhereCount, updateWhereCount) + import Handler.Utils import Handler.Utils.ExternalExam.Users @@ -16,31 +19,38 @@ postEEGradesR tid ssh coursen examn = do (usersResult, examUsersTable) <- makeExternalExamUsersTable EEUMGrades eExam usersResult' <- formResultMaybe usersResult $ \case - (ExternalExamUserMarkSynchronisedData, selectedResults) -> do - forM_ selectedResults externalExamResultMarkSynchronised + (ExternalExamUserMarkSynchronisedData, selectedUsers) -> do + forM_ selectedUsers externalExamResultMarkSynchronised return . Just $ do - addMessageI Success $ MsgExamUserMarkedSynchronised (length selectedResults) + addMessageI Success $ MsgExamUserMarkedSynchronised $ length selectedUsers redirect $ EExamR tid ssh coursen examn EEGradesR - (ExternalExamUserEditOccurrenceData occ, selectedResults) -> do - -- TODO work in progress - forM_ selectedResults $ externalExamEditOccurrence occ + (ExternalExamUserEditOccurrenceData occ, selectedUsers) -> do + nrEdited <- do + now <- liftIO getCurrentTime + updateWhereCount [ ExternalExamResultId <-. Set.toList selectedUsers ] + [ ExternalExamResultTime =. occ + , ExternalExamResultLastChanged =. now + ] return . Just $ do - addMessageI Success $ MsgExternalExamOccurrenceEdited $ length selectedResults + addMessageI Success $ MsgExternalExamOccurrenceEdited nrEdited redirect $ EExamR tid ssh coursen examn EEGradesR - (ExternalExamUserEditResultData examResult, selectedResults) -> do - -- TODO work in progress - forM_ selectedResults $ externalExamEditResult examResult + (ExternalExamUserEditResultData examResult, selectedUsers) -> do + nrEdited <- do + now <- liftIO getCurrentTime + updateWhereCount [ ExternalExamResultId <-. Set.toList selectedUsers ] + [ ExternalExamResultResult =. examResult + , ExternalExamResultLastChanged =. now + ] return . Just $ do - addMessageI Success $ MsgExternalExamResultEdited $ length selectedResults + addMessageI Success $ MsgExternalExamResultEdited nrEdited redirect $ EExamR tid ssh coursen examn EEGradesR - (ExternalExamUserDeleteData, selectedResults) -> do - -- TODO work in progress - forM_ selectedResults externalExamDeleteUser + (ExternalExamUserDeleteData, selectedUsers) -> do + nrDeleted <- deleteWhereCount [ ExternalExamResultId <-. Set.toList selectedUsers ] return . Just $ do - addMessageI Success $ MsgExternalExamUserDeleted $ length selectedResults + addMessageI Success $ MsgExternalExamUserDeleted nrDeleted redirect $ EExamR tid ssh coursen examn EEGradesR return (usersResult', examUsersTable) diff --git a/src/Handler/Utils/ExternalExam/Users.hs b/src/Handler/Utils/ExternalExam/Users.hs index 84112e019..9f0a022ed 100644 --- a/src/Handler/Utils/ExternalExam/Users.hs +++ b/src/Handler/Utils/ExternalExam/Users.hs @@ -311,9 +311,21 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do return (formRes, formWgt) EEUMUsers -> \csrf -> do let + -- TODO work in progress actionMap :: Map ExternalExamUserAction (AForm Handler ExternalExamUserActionData) - actionMap = Map.fromList - [] + actionMap = mconcat + [ singletonMap ExternalExamUserMarkSynchronised $ + pure ExternalExamUserMarkSynchronisedData + --, singletonMap ExternalExamUserEditOccurrence $ + -- ExternalExamUserEditOccurrenceData + -- <$> mempty + -- <$> aopt () (fslI MsgExamOccurrence) (Just Nothing) + --, singletonMap ExternalExamUserEditResult $ + -- ExternalExamUserEditResultData + -- <$> mempty + , singletonMap ExternalExamUserDelete $ + pure ExternalExamUserDeleteData + ] (res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf let formRes = (, mempty) . First . Just <$> res return (formRes, formWgt) @@ -483,21 +495,3 @@ externalExamResultMarkSynchronised resId = do | Entity _ UserFunction{..} <- userFunctions ] -externalExamEditOccurrence :: UTCTime -> ExternalExamResultId -> DB () -externalExamEditOccurrence _ _ = do - --uid <- requireAuthId - --now <- liftIO getCurrentTime - -- TODO work in progress - return () - -externalExamEditResult :: ExamResultPassedGrade -> ExternalExamResultId -> DB () -externalExamEditResult _ _ = do - --uid <- requireAuthId - --now <- liftIO getCurrentTime - -- TODO work in progress - return () - -externalExamDeleteUser :: ExternalExamResultId -> DB () -externalExamDeleteUser _ = do - -- TODO work in progress - return () From baa3fd82e1642e5527b94cf71bc6e96dc0f81455 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 3 Mar 2020 16:32:56 +0100 Subject: [PATCH 004/720] feat(eeusersr): audit external exam result delete --- src/Handler/ExamOffice/ExternalExam.hs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Handler/ExamOffice/ExternalExam.hs b/src/Handler/ExamOffice/ExternalExam.hs index 40a040fc8..a98836fab 100644 --- a/src/Handler/ExamOffice/ExternalExam.hs +++ b/src/Handler/ExamOffice/ExternalExam.hs @@ -19,16 +19,16 @@ postEEGradesR tid ssh coursen examn = do (usersResult, examUsersTable) <- makeExternalExamUsersTable EEUMGrades eExam usersResult' <- formResultMaybe usersResult $ \case - (ExternalExamUserMarkSynchronisedData, selectedUsers) -> do - forM_ selectedUsers externalExamResultMarkSynchronised + (ExternalExamUserMarkSynchronisedData, selectedResults) -> do + forM_ selectedResults externalExamResultMarkSynchronised return . Just $ do - addMessageI Success $ MsgExamUserMarkedSynchronised $ length selectedUsers + addMessageI Success $ MsgExamUserMarkedSynchronised $ length selectedResults redirect $ EExamR tid ssh coursen examn EEGradesR - (ExternalExamUserEditOccurrenceData occ, selectedUsers) -> do + (ExternalExamUserEditOccurrenceData occ, selectedResults) -> do nrEdited <- do now <- liftIO getCurrentTime - updateWhereCount [ ExternalExamResultId <-. Set.toList selectedUsers ] + updateWhereCount [ ExternalExamResultId <-. Set.toList selectedResults ] [ ExternalExamResultTime =. occ , ExternalExamResultLastChanged =. now ] @@ -36,10 +36,10 @@ postEEGradesR tid ssh coursen examn = do addMessageI Success $ MsgExternalExamOccurrenceEdited nrEdited redirect $ EExamR tid ssh coursen examn EEGradesR - (ExternalExamUserEditResultData examResult, selectedUsers) -> do + (ExternalExamUserEditResultData examResult, selectedResults) -> do nrEdited <- do now <- liftIO getCurrentTime - updateWhereCount [ ExternalExamResultId <-. Set.toList selectedUsers ] + updateWhereCount [ ExternalExamResultId <-. Set.toList selectedResults ] [ ExternalExamResultResult =. examResult , ExternalExamResultLastChanged =. now ] @@ -47,8 +47,15 @@ postEEGradesR tid ssh coursen examn = do addMessageI Success $ MsgExternalExamResultEdited nrEdited redirect $ EExamR tid ssh coursen examn EEGradesR - (ExternalExamUserDeleteData, selectedUsers) -> do - nrDeleted <- deleteWhereCount [ ExternalExamResultId <-. Set.toList selectedUsers ] + (ExternalExamUserDeleteData, selectedResults) -> do + nrDeleted <- fmap getSum . flip foldMapM selectedResults $ \selectedResult -> do + mExtExamRes <- get selectedResult + nrDel <- deleteWhereCount [ ExternalExamResultId ==. selectedResult ] + if + | Just ExternalExamResult{..} <- mExtExamRes -> do + forM_ [1..nrDel] $ const $ audit $ TransactionExternalExamResultDelete externalExamResultExam externalExamResultUser + return $ Sum nrDel + | otherwise -> return mempty return . Just $ do addMessageI Success $ MsgExternalExamUserDeleted nrDeleted redirect $ EExamR tid ssh coursen examn EEGradesR From 0d54757d16bebadaeceeb0947df65f15d86ac3e0 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 3 Mar 2020 16:40:15 +0100 Subject: [PATCH 005/720] feat(eeusersr): audit external exam result result edit --- src/Handler/ExamOffice/ExternalExam.hs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Handler/ExamOffice/ExternalExam.hs b/src/Handler/ExamOffice/ExternalExam.hs index a98836fab..3c9c4bcf1 100644 --- a/src/Handler/ExamOffice/ExternalExam.hs +++ b/src/Handler/ExamOffice/ExternalExam.hs @@ -37,12 +37,19 @@ postEEGradesR tid ssh coursen examn = do redirect $ EExamR tid ssh coursen examn EEGradesR (ExternalExamUserEditResultData examResult, selectedResults) -> do - nrEdited <- do + nrEdited <- fmap getSum . flip foldMapM selectedResults $ \selectedResult -> do now <- liftIO getCurrentTime - updateWhereCount [ ExternalExamResultId <-. Set.toList selectedResults ] + mExtExamRes <- get selectedResult + nrEdit <- updateWhereCount + [ ExternalExamResultId ==. selectedResult ] [ ExternalExamResultResult =. examResult , ExternalExamResultLastChanged =. now ] + if + | Just ExternalExamResult{..} <- mExtExamRes -> do + forM_ [1..nrEdit] $ const $ audit $ TransactionExternalExamResultEdit externalExamResultExam externalExamResultUser + return $ Sum nrEdit + | otherwise -> return mempty return . Just $ do addMessageI Success $ MsgExternalExamResultEdited nrEdited redirect $ EExamR tid ssh coursen examn EEGradesR From ed3f761b24f98527ca0b26b09db7a75f1c98142e Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 3 Mar 2020 16:52:13 +0100 Subject: [PATCH 006/720] feat(eeusersr): audit external exam result result and occurrence edits --- src/Handler/ExamOffice/ExternalExam.hs | 37 ++++++++++++-------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/Handler/ExamOffice/ExternalExam.hs b/src/Handler/ExamOffice/ExternalExam.hs index 3c9c4bcf1..949399545 100644 --- a/src/Handler/ExamOffice/ExternalExam.hs +++ b/src/Handler/ExamOffice/ExternalExam.hs @@ -4,7 +4,6 @@ module Handler.ExamOffice.ExternalExam import Import -import qualified Data.Set as Set import Database.Persist.Sql (deleteWhereCount, updateWhereCount) import Handler.Utils @@ -18,6 +17,19 @@ postEEGradesR tid ssh coursen examn = do eExam <- getBy404 $ UniqueExternalExam tid ssh coursen examn (usersResult, examUsersTable) <- makeExternalExamUsersTable EEUMGrades eExam + let + editResults results changeList = fmap getSum . flip foldMapM results $ \result -> do + now <- liftIO getCurrentTime + mExtExamRes <- get result + nrEdit <- updateWhereCount + [ ExternalExamResultId ==. result ] + (changeList <> [ ExternalExamResultLastChanged =. now ]) + if + | Just ExternalExamResult{..} <- mExtExamRes -> do + forM_ [1..nrEdit] $ const $ audit $ TransactionExternalExamResultEdit externalExamResultExam externalExamResultUser + return $ Sum nrEdit + | otherwise -> return mempty + usersResult' <- formResultMaybe usersResult $ \case (ExternalExamUserMarkSynchronisedData, selectedResults) -> do forM_ selectedResults externalExamResultMarkSynchronised @@ -26,30 +38,15 @@ postEEGradesR tid ssh coursen examn = do redirect $ EExamR tid ssh coursen examn EEGradesR (ExternalExamUserEditOccurrenceData occ, selectedResults) -> do - nrEdited <- do - now <- liftIO getCurrentTime - updateWhereCount [ ExternalExamResultId <-. Set.toList selectedResults ] - [ ExternalExamResultTime =. occ - , ExternalExamResultLastChanged =. now - ] + nrEdited <- editResults selectedResults + [ ExternalExamResultTime =. occ ] return . Just $ do addMessageI Success $ MsgExternalExamOccurrenceEdited nrEdited redirect $ EExamR tid ssh coursen examn EEGradesR (ExternalExamUserEditResultData examResult, selectedResults) -> do - nrEdited <- fmap getSum . flip foldMapM selectedResults $ \selectedResult -> do - now <- liftIO getCurrentTime - mExtExamRes <- get selectedResult - nrEdit <- updateWhereCount - [ ExternalExamResultId ==. selectedResult ] - [ ExternalExamResultResult =. examResult - , ExternalExamResultLastChanged =. now - ] - if - | Just ExternalExamResult{..} <- mExtExamRes -> do - forM_ [1..nrEdit] $ const $ audit $ TransactionExternalExamResultEdit externalExamResultExam externalExamResultUser - return $ Sum nrEdit - | otherwise -> return mempty + nrEdited <- editResults selectedResults + [ ExternalExamResultResult =. examResult ] return . Just $ do addMessageI Success $ MsgExternalExamResultEdited nrEdited redirect $ EExamR tid ssh coursen examn EEGradesR From 7540a4fe5fe0f61d449bc7cc5f5aa7d3da034f55 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Wed, 15 Apr 2020 16:32:45 +0200 Subject: [PATCH 007/720] feat(mass-input): automatic add before submit --- frontend/src/utils/mass-input/mass-input.js | 113 ++++++++++++++---- frontend/src/utils/mass-input/mass-input.sass | 13 ++ 2 files changed, 103 insertions(+), 23 deletions(-) diff --git a/frontend/src/utils/mass-input/mass-input.js b/frontend/src/utils/mass-input/mass-input.js index dee124a1a..84850cc2a 100644 --- a/frontend/src/utils/mass-input/mass-input.js +++ b/frontend/src/utils/mass-input/mass-input.js @@ -1,3 +1,5 @@ +/* global global:writable */ + import { Utility } from '../../core/utility'; import { Datepicker } from '../form/datepicker'; import './mass-input.sass'; @@ -7,6 +9,11 @@ const MASS_INPUT_ADD_CELL_SELECTOR = '.massinput__cell--add'; const MASS_INPUT_SUBMIT_BUTTON_CLASS = 'massinput__submit-button'; const MASS_INPUT_INITIALIZED_CLASS = 'mass-input--initialized'; +const MASS_INPUT_ADD_CHANGE_FIELD_SELECTOR = 'select, input[type=radio]'; + +// const MASS_INPUT_SAFETY_SUBMITTED_CLASS = 'massinput--safety-submitted'; +// const MASS_INPUT_SAFETY_SUBMITTED_TIMEOUT = 1000; + @Utility({ selector: '[uw-mass-input]', }) @@ -14,11 +21,14 @@ export class MassInput { _element; _app; + _global; _massInputId; _massInputFormSubmitHandler; _massInputForm; + _changedAdd = new Array(); + constructor(element, app) { if (!element) { throw new Error('Mass Input utility cannot be setup without an element!'); @@ -27,6 +37,14 @@ export class MassInput { this._element = element; this._app = app; + if (global !== undefined) + this._global = global; + else if (window !== undefined) + this._global = window; + else + throw new Error('Cannot setup Mass Input utility without window or global'); + + if (this._element.classList.contains(MASS_INPUT_INITIALIZED_CLASS)) { return false; } @@ -47,8 +65,10 @@ export class MassInput { this._setupSubmitButton(button); }); - this._massInputForm.addEventListener('submit', this._massInputFormSubmitHandler); - this._massInputForm.addEventListener('keypress', this._keypressHandler); + this._massInputForm.addEventListener('submit', this._massInputFormSubmitHandler.bind(this)); + this._massInputForm.addEventListener('keypress', this._keypressHandler.bind(this)); + + Array.from(this._element.querySelectorAll(MASS_INPUT_ADD_CELL_SELECTOR)).forEach(this._setupChangedHandlers.bind(this)); // mark initialized this._element.classList.add(MASS_INPUT_INITIALIZED_CLASS); @@ -58,6 +78,26 @@ export class MassInput { this._reset(); } + _setupChangedHandlers(addCell) { + Array.from(addCell.querySelectorAll(MASS_INPUT_ADD_CHANGE_FIELD_SELECTOR)).forEach(inputElem => { + if (inputElem.closest('[uw-mass-input]') !== this._element) + return; + + inputElem.addEventListener('change', () => { this._changedAdd.push(addCell); }); + }); + } + + _unsafeAddCells() { + let changedAdd = this._changedAdd; + + Array.from(this._element.querySelectorAll(MASS_INPUT_ADD_CELL_SELECTOR)).forEach(addCell => addCell.querySelectorAll('input:not([type=checkbox]):not([type=radio])').forEach(inputElem => { + if (inputElem.closest('[uw-mass-input]') === this._element && inputElem.value !== '') + changedAdd.push(addCell); + })); + + return changedAdd; + } + _makeSubmitHandler() { const method = this._massInputForm.getAttribute('method') || 'POST'; const url = this._massInputForm.getAttribute('action') || window.location.href; @@ -69,31 +109,58 @@ export class MassInput { } return (event) => { - let activeElement; + let submitButton; + let isAddCell; + + let isMassInputSubmit = (() => { + let activeElement; - // check if event occured from either a mass input add/delete button or - // from inside one of massinput's inputs (i.e. a child is focused/active) - activeElement = this._element.querySelector(':focus, :active'); + // check if event occured from either a mass input add/delete button or + // from inside one of massinput's inputs (i.e. a child is focused/active) + activeElement = this._element.querySelector(':focus, :active'); - if (!activeElement) { - return false; + if (!activeElement) { + return false; + } + + // find the according massinput cell thats hosts the element that triggered the submit + const massInputCell = activeElement.closest(MASS_INPUT_CELL_SELECTOR); + if (!massInputCell) { + return false; + } + + submitButton = massInputCell.querySelector('.' + MASS_INPUT_SUBMIT_BUTTON_CLASS); + if (!submitButton) { + return false; + } + + isAddCell = massInputCell.matches(MASS_INPUT_ADD_CELL_SELECTOR); + const submitButtonIsActive = submitButton.matches(':focus, :active'); + // if the cell is not an add cell the active element must at least be the cells submit button + if (!isAddCell && !submitButtonIsActive) { + return false; + } + + return true; + })(); + + let unsafeAddCells = this._unsafeAddCells(); + + if (unsafeAddCells.length > 0 && !isMassInputSubmit) { + let addButtons = Array.from(unsafeAddCells[0].querySelectorAll('.' + MASS_INPUT_SUBMIT_BUTTON_CLASS)).filter(addButton => addButton.closest('[uw-mass-input]') === this._element); + + if (addButtons.length > 0) { + submitButton = addButtons[0]; + isMassInputSubmit = true; + isAddCell = false; + + this._element.scrollIntoView(); + // this._element.classList.add(MASS_INPUT_SAFETY_SUBMITTED_CLASS); + // this._global.setTimeout(() => { this._element.classList.remove(MASS_INPUT_SAFETY_SUBMITTED_CLASS) }, MASS_INPUT_SAFETY_SUBMITTED_TIMEOUT) + } } - // find the according massinput cell thats hosts the element that triggered the submit - const massInputCell = activeElement.closest(MASS_INPUT_CELL_SELECTOR); - if (!massInputCell) { - return false; - } - - const submitButton = massInputCell.querySelector('.' + MASS_INPUT_SUBMIT_BUTTON_CLASS); - if (!submitButton) { - return false; - } - - const isAddCell = massInputCell.matches(MASS_INPUT_ADD_CELL_SELECTOR); - const submitButtonIsActive = submitButton.matches(':focus, :active'); - // if the cell is not an add cell the active element must at least be the cells submit button - if (!isAddCell && !submitButtonIsActive) { + if (!isMassInputSubmit) { return false; } diff --git a/frontend/src/utils/mass-input/mass-input.sass b/frontend/src/utils/mass-input/mass-input.sass index 89bdf08c9..1339a2253 100644 --- a/frontend/src/utils/mass-input/mass-input.sass +++ b/frontend/src/utils/mass-input/mass-input.sass @@ -1,3 +1,5 @@ +@use "../../app" as * + .massinput-list__wrapper, .massinput-list__cell display: grid grid: auto / auto 50px @@ -12,3 +14,14 @@ .massinput-list__cell grid-column: 1 / 3 + +/* .massinput--safety-submitted +/* animation: massinput--safety-submitted linear 1s + +/* @keyframes massinput--safety-submitted +/* 0% +/* background-color: rgba(252, 153, 0, 0) +/* 50% +/* background-color: rgba(252, 153, 0, 0.8) +/* 100% +/* background-color: rgba(252, 153, 0, 0) From 0b3c88407b096ce25bebff0311f0898f92868530 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Thu, 16 Apr 2020 09:26:37 +0200 Subject: [PATCH 008/720] refactor(course-user): modularize --- src/Foundation.hs | 25 +++ src/Handler/Course/User.hs | 192 ++++++++++-------- templates/course/user/note.hamlet | 3 + .../user/profile.hamlet} | 3 - 4 files changed, 132 insertions(+), 91 deletions(-) create mode 100644 templates/course/user/note.hamlet rename templates/{course-user.hamlet => course/user/profile.hamlet} (98%) diff --git a/src/Foundation.hs b/src/Foundation.hs index 8cbe2940a..878510f3d 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1101,6 +1101,14 @@ tagAccessPredicate AuthParticipant = APDB $ \mAuthId route _ -> case route of E.&&. course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseShorthand E.==. E.val csh + -- participant is exam corrector for this course + mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` exam `E.InnerJoin` examCorrector) -> do + E.on $ exam E.^. ExamId E.==. examCorrector E.^. ExamCorrectorExam + E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse + E.where_ $ examCorrector E.^. ExamCorrectorUser E.==. E.val participant + E.&&. course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh -- participant is lecturer for this course mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` lecturer) -> do E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse @@ -1108,6 +1116,23 @@ tagAccessPredicate AuthParticipant = APDB $ \mAuthId route _ -> case route of E.&&. course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseShorthand E.==. E.val csh + -- participant has an exam result for this course + mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do + E.on $ examResult E.^. ExamResultExam E.==. exam E.^. ExamId + E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse + E.where_ $ examResult E.^. ExamResultUser E.==. E.val participant + E.&&. course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh + -- participant is registered for an exam for this course + mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do + E.on $ examRegistration E.^. ExamRegistrationExam E.==. exam E.^. ExamId + E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse + E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val participant + E.&&. course E.^. CourseTerm E.==. E.val tid + E.&&. course E.^. CourseSchool E.==. E.val ssh + E.&&. course E.^. CourseShorthand E.==. E.val csh + return () tagAccessPredicate AuthApplicant = APDB $ \mAuthId route _ -> case route of diff --git a/src/Handler/Course/User.hs b/src/Handler/Course/User.hs index aef35f333..240c80cec 100644 --- a/src/Handler/Course/User.hs +++ b/src/Handler/Course/User.hs @@ -20,80 +20,53 @@ import Jobs.Queue getCUserR, postCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html getCUserR = postCUserR postCUserR tid ssh csh uCId = do - -- Has authorization checks (OR): - -- - -- - User is current member of course - -- - User has submitted in course - -- - User is member of registered group for course - -- - User is member of a tutorial for course - -- - User is corrector for course - -- - User is a tutor for course - -- - User is a lecturer for course - let currentRoute = CourseR tid ssh csh (CUserR uCId) - Entity dozentId (userShowSex -> showSex) <- requireAuth - uid <- decrypt uCId - -- DB reads - (cid, User{..}, mRegistration, thisUniqueNote, noteText, noteEdits, studies) <- runDB $ do - cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh - -- Abfrage Benutzerdaten + showSex <- maybe False (userShowSex . entityVal) <$> maybeAuth + + (course, user@(Entity _ User{..}), registered) <- runDB $ do + uid <- decrypt uCId + course@(Entity cid _) <- getBy404 $ TermSchoolCourseShort tid ssh csh user <- get404 uid - registration <- getBy (UniqueParticipant uid cid) - -- Abfrage Teilnehmernotiz - let thisUniqueNote = UniqueCourseUserNote uid cid - mbNoteEnt <- getBy thisUniqueNote - (noteText,noteEdits) <- case mbNoteEnt of - Nothing -> return (Nothing,[]) - (Just (Entity noteKey CourseUserNote{courseUserNoteNote})) -> do - noteEdits <- E.select $ E.from $ \(edit `E.InnerJoin` usr) -> do - E.on $ edit E.^. CourseUserNoteEditUser E.==. usr E.^. UserId - E.where_ $ edit E.^. CourseUserNoteEditNote E.==. E.val noteKey - E.orderBy [E.desc $ edit E.^. CourseUserNoteEditTime] - E.limit 1 -- more will be shown, if changed here - return (edit E.^. CourseUserNoteEditTime, usr E.^. UserEmail, usr E.^. UserDisplayName, usr E.^. UserSurname) - return (Just courseUserNoteNote, $(unValueN 4) <$> noteEdits) - -- Abfrage Studiengänge + registered <- existsBy $ UniqueParticipant uid cid + + return (course, Entity uid user, registered) + + sections <- mapM (runMaybeT . ($ user) . ($ course)) + [ courseUserProfileSection + , courseUserNoteSection + ] + + -- generate output + let headingLong + | registered + , Just sex <- guardOn showSex =<< userSex + = [whamlet|^{nameWidget userDisplayName userSurname} (_{ShortSex sex}), _{MsgCourseMemberOf} #{csh} #{tid}|] + | registered + = [whamlet|^{nameWidget userDisplayName userSurname}, _{MsgCourseMemberOf} #{csh} #{tid}|] + | Just sex <- guardOn showSex =<< userSex + = [whamlet|^{nameWidget userDisplayName userSurname} (_{ShortSex sex}), _{MsgCourseAssociatedWith} #{csh} #{tid}|] + | otherwise + = [whamlet|^{nameWidget userDisplayName userSurname}, _{MsgCourseAssociatedWith} #{csh} #{tid}|] + headingShort = prependCourseTitle tid ssh csh $ SomeMessage userDisplayName + siteLayout headingLong $ do + setTitleI headingShort + + forM_ sections . fromMaybe $ return () + +courseUserProfileSection :: Entity Course -> Entity User -> MaybeT Handler Widget +courseUserProfileSection (Entity cid Course{..}) (Entity uid User{ userShowSex = _, ..}) = do + showSex <- maybe False (userShowSex . entityVal) <$> maybeAuth + currentRoute <- MaybeT getCurrentRoute + + (mRegistration, studies) <- lift . runDB $ do + registration <- getBy $ UniqueParticipant uid cid studies <- E.select $ E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid E.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId return (studyfeat, studydegree, studyterms) - return (cid, user, registration, thisUniqueNote, noteText, noteEdits, studies) - let editByWgt = [whamlet| - $newline never -