From 15689c597ef407583b01dabc9f7631e9dc90b009 Mon Sep 17 00:00:00 2001 From: ros Date: Mon, 5 Jul 2021 12:53:17 +0200 Subject: [PATCH 001/143] feat(course admin): done --- messages/uniworx/categories/courses/courses/de-de-formal.msg | 3 ++- messages/uniworx/categories/courses/courses/en-eu.msg | 1 + src/Application.hs | 1 + src/Model/Types/Course.hs | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/messages/uniworx/categories/courses/courses/de-de-formal.msg b/messages/uniworx/categories/courses/courses/de-de-formal.msg index fb33d8c9f..cb324e1dc 100644 --- a/messages/uniworx/categories/courses/courses/de-de-formal.msg +++ b/messages/uniworx/categories/courses/courses/de-de-formal.msg @@ -277,4 +277,5 @@ MailSubjectLecturerInvitation tid@TermId ssh@SchoolId csh@CourseShorthand: [#{ti LecturerInvitationAccepted lType@Text csh@CourseShorthand: Sie wurden als #{lType} für #{csh} eingetragen CourseExamRegistrationTime: Angemeldet seit CourseParticipantStateIsActiveFilter: Ansicht -CourseApply: Zum Kurs bewerben \ No newline at end of file +CourseApply: Zum Kurs bewerben +CourseAdministrator: Administrator:in \ No newline at end of file diff --git a/messages/uniworx/categories/courses/courses/en-eu.msg b/messages/uniworx/categories/courses/courses/en-eu.msg index d3e3d709f..96c84588f 100644 --- a/messages/uniworx/categories/courses/courses/en-eu.msg +++ b/messages/uniworx/categories/courses/courses/en-eu.msg @@ -277,3 +277,4 @@ LecturerInvitationAccepted lType csh: You were registered as #{lType} for #{csh} CourseExamRegistrationTime: Registered since CourseParticipantStateIsActiveFilter: View CourseApply: Apply for course +CourseAdministrator: Administrator \ No newline at end of file diff --git a/src/Application.hs b/src/Application.hs index 0c0fcbbd5..426fc930d 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -710,3 +710,4 @@ addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do PWHashConf{..} <- getsYesod $ view _appAuthPWHash (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength void $ insert User{..} + diff --git a/src/Model/Types/Course.hs b/src/Model/Types/Course.hs index e9779a486..7ede7a7e7 100644 --- a/src/Model/Types/Course.hs +++ b/src/Model/Types/Course.hs @@ -14,7 +14,7 @@ import Model.Types.TH.PathPiece import Utils.Lens.TH -data LecturerType = CourseLecturer | CourseAssistant +data LecturerType = CourseLecturer | CourseAssistant | CourseAdministrator deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Universe, Finite, NFData) From 89e1d675c3be0fec106e84920184a8c95dfa6346 Mon Sep 17 00:00:00 2001 From: ros Date: Thu, 8 Jul 2021 11:01:09 +0200 Subject: [PATCH 002/143] feat(lecturer type): aenderung --- src/Handler/Tutorial/List.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Handler/Tutorial/List.hs b/src/Handler/Tutorial/List.hs index 39f67c0e8..b49eea883 100644 --- a/src/Handler/Tutorial/List.hs +++ b/src/Handler/Tutorial/List.hs @@ -42,7 +42,7 @@ getCTutorialListR tid ssh csh = do dbtColonnade = dbColonnade $ mconcat [ sortable (Just "type") (i18nCell MsgTableTutorialType) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) -> textCell $ CI.original tutorialType , sortable (Just "name") (i18nCell MsgTableTutorialName) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) -> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) [whamlet|#{tutorialName}|] - , sortable Nothing (i18nCell MsgTableTutorialTutors) $ \(view $ resultTutorial . _entityKey -> tutid) -> sqlCell $ do + , sortable (Just "tutors") (i18nCell MsgTableTutorialTutors) $ \(view $ resultTutorial . _entityKey -> tutid) -> sqlCell $ do tutors <- fmap (map $(unValueN 3)) . E.select . E.from $ \(tutor `E.InnerJoin` user) -> do E.on $ tutor E.^. TutorUser E.==. user E.^. UserId E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid From 2321216b0f4f194c7cd8b47eb020819d6aa1f2e5 Mon Sep 17 00:00:00 2001 From: ros Date: Thu, 8 Jul 2021 14:12:05 +0200 Subject: [PATCH 003/143] feat(link password time): done --- messages/uniworx/categories/jobs_handler/de-de-formal.msg | 1 + messages/uniworx/categories/jobs_handler/en-eu.msg | 1 + src/Application.hs | 2 +- src/Foundation/I18n.hs | 2 ++ templates/mail/passwordReset.hamlet | 1 + 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/uniworx/categories/jobs_handler/de-de-formal.msg b/messages/uniworx/categories/jobs_handler/de-de-formal.msg index 9486409fa..7a63c78ff 100644 --- a/messages/uniworx/categories/jobs_handler/de-de-formal.msg +++ b/messages/uniworx/categories/jobs_handler/de-de-formal.msg @@ -17,3 +17,4 @@ InvitationAcceptDecline: Einladung annehmen/ablehnen InvitationFromTip displayName@Text: Sie erhalten diese Einladung, weil #{displayName} ihren Versand in Uni2work ausgelöst hat. InvitationFromTipAnonymous: Sie erhalten diese Einladung, weil ein nicht eingeloggter Benutzer/eine nichteingeloggte Benutzerin ihren Versand in Uni2work ausgelöst hat. InvitationUniWorXTip: Uni2work ist ein webbasiertes Lehrverwaltungssystem der LMU München. +LinkActiveUntil time@UTCTime: Der Link ist nur bis #{time} aktiv! \ No newline at end of file diff --git a/messages/uniworx/categories/jobs_handler/en-eu.msg b/messages/uniworx/categories/jobs_handler/en-eu.msg index 6df1adf0c..f7df35617 100644 --- a/messages/uniworx/categories/jobs_handler/en-eu.msg +++ b/messages/uniworx/categories/jobs_handler/en-eu.msg @@ -17,3 +17,4 @@ InvitationAcceptDecline: Accept/Decline invitation InvitationFromTip displayName: You are receiving this invitation because #{displayName} has caused it to be sent from within Uni2work. InvitationFromTipAnonymous: You are receiving this invitiation because an user who didn't log in has caused it to be send from within Uni2work. InvitationUniWorXTip: Uni2work is a web based teaching management system at LMU Munich. +LinkActiveUntil time@UTCTime: The link is only available until #{time}! \ No newline at end of file diff --git a/src/Application.hs b/src/Application.hs index bcaf1edda..ab3bb8886 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -707,4 +707,4 @@ addPWEntry :: User addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do PWHashConf{..} <- getsYesod $ view _appAuthPWHash (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength - void $ insert User{..} + void $ insert User{..} \ No newline at end of file diff --git a/src/Foundation/I18n.hs b/src/Foundation/I18n.hs index b720355c6..e013060f5 100644 --- a/src/Foundation/I18n.hs +++ b/src/Foundation/I18n.hs @@ -417,6 +417,8 @@ instance ToMessage Natural where toMessage = tshow instance ToMessage Word64 where toMessage = tshow +instance ToMessage UTCTime where + toMessage = tshow instance HasResolution a => ToMessage (Fixed a) where toMessage = toMessage . showFixed True diff --git a/templates/mail/passwordReset.hamlet b/templates/mail/passwordReset.hamlet index 2dd9dfc7b..df7524f81 100644 --- a/templates/mail/passwordReset.hamlet +++ b/templates/mail/passwordReset.hamlet @@ -13,3 +13,4 @@ $newline never

_{SomeMessage MsgResetPassword} + _{SomeMessage $ MsgLinkActiveUntil tomorrowEndOfDay} From 0a6a1749d351e626383e513293af280f78552009 Mon Sep 17 00:00:00 2001 From: ros Date: Thu, 22 Jul 2021 21:01:54 +0200 Subject: [PATCH 004/143] feat(course admin): no new-line --- src/Application.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Application.hs b/src/Application.hs index 426fc930d..001d87096 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -709,5 +709,4 @@ addPWEntry :: User addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do PWHashConf{..} <- getsYesod $ view _appAuthPWHash (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength - void $ insert User{..} - + void $ insert User{..} \ No newline at end of file From df2a9bc20fe9f958cbee98315b644ec2fcba0630 Mon Sep 17 00:00:00 2001 From: ros Date: Fri, 23 Jul 2021 09:53:40 +0200 Subject: [PATCH 005/143] feat(link password time): new time format --- messages/uniworx/categories/jobs_handler/de-de-formal.msg | 2 +- messages/uniworx/categories/jobs_handler/en-eu.msg | 2 +- src/Foundation/I18n.hs | 3 --- src/Jobs/Handler/SendPasswordReset.hs | 4 ++-- templates/mail/passwordReset.hamlet | 2 +- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/messages/uniworx/categories/jobs_handler/de-de-formal.msg b/messages/uniworx/categories/jobs_handler/de-de-formal.msg index 7a63c78ff..8fd6899f6 100644 --- a/messages/uniworx/categories/jobs_handler/de-de-formal.msg +++ b/messages/uniworx/categories/jobs_handler/de-de-formal.msg @@ -17,4 +17,4 @@ InvitationAcceptDecline: Einladung annehmen/ablehnen InvitationFromTip displayName@Text: Sie erhalten diese Einladung, weil #{displayName} ihren Versand in Uni2work ausgelöst hat. InvitationFromTipAnonymous: Sie erhalten diese Einladung, weil ein nicht eingeloggter Benutzer/eine nichteingeloggte Benutzerin ihren Versand in Uni2work ausgelöst hat. InvitationUniWorXTip: Uni2work ist ein webbasiertes Lehrverwaltungssystem der LMU München. -LinkActiveUntil time@UTCTime: Der Link ist nur bis #{time} aktiv! \ No newline at end of file +LinkActiveUntil time@Text: Der Link ist nur bis #{time} aktiv! \ No newline at end of file diff --git a/messages/uniworx/categories/jobs_handler/en-eu.msg b/messages/uniworx/categories/jobs_handler/en-eu.msg index f7df35617..be3093426 100644 --- a/messages/uniworx/categories/jobs_handler/en-eu.msg +++ b/messages/uniworx/categories/jobs_handler/en-eu.msg @@ -17,4 +17,4 @@ InvitationAcceptDecline: Accept/Decline invitation InvitationFromTip displayName: You are receiving this invitation because #{displayName} has caused it to be sent from within Uni2work. InvitationFromTipAnonymous: You are receiving this invitiation because an user who didn't log in has caused it to be send from within Uni2work. InvitationUniWorXTip: Uni2work is a web based teaching management system at LMU Munich. -LinkActiveUntil time@UTCTime: The link is only available until #{time}! \ No newline at end of file +LinkActiveUntil time@Text: The link is only available until #{time}! \ No newline at end of file diff --git a/src/Foundation/I18n.hs b/src/Foundation/I18n.hs index e013060f5..0d7a31ee1 100644 --- a/src/Foundation/I18n.hs +++ b/src/Foundation/I18n.hs @@ -417,9 +417,6 @@ instance ToMessage Natural where toMessage = tshow instance ToMessage Word64 where toMessage = tshow -instance ToMessage UTCTime where - toMessage = tshow - instance HasResolution a => ToMessage (Fixed a) where toMessage = toMessage . showFixed True diff --git a/src/Jobs/Handler/SendPasswordReset.hs b/src/Jobs/Handler/SendPasswordReset.hs index 0832b4453..5e73b8949 100644 --- a/src/Jobs/Handler/SendPasswordReset.hs +++ b/src/Jobs/Handler/SendPasswordReset.hs @@ -34,7 +34,7 @@ dispatchJobSendPasswordReset jRecipient = JobHandlerException . userMailT jRecip let resetBearer = resetBearer' & bearerRestrict (UserPasswordR cID) (decodeUtf8 . Base64.encode . BA.convert $ computeUserAuthenticationDigest userAuthentication) encodedBearer <- encodeBearer resetBearer - + resetUrl <- toTextUrl (UserPasswordR cID, [(toPathPiece GetBearer, toPathPiece encodedBearer)]) - + activeTime <- formatTimeMail SelFormatDateTime tomorrowEndOfDay addHtmlMarkdownAlternatives ($(ihamletFile "templates/mail/passwordReset.hamlet") :: HtmlUrlI18n (SomeMessage UniWorX) (Route UniWorX)) diff --git a/templates/mail/passwordReset.hamlet b/templates/mail/passwordReset.hamlet index df7524f81..fa448db58 100644 --- a/templates/mail/passwordReset.hamlet +++ b/templates/mail/passwordReset.hamlet @@ -13,4 +13,4 @@ $newline never

_{SomeMessage MsgResetPassword} - _{SomeMessage $ MsgLinkActiveUntil tomorrowEndOfDay} + _{SomeMessage $ MsgLinkActiveUntil activeTime} From 26e638dfd777b04161e0da026a532d4446ae3c0f Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 12 Aug 2021 12:02:58 +0200 Subject: [PATCH 006/143] Apply 1 suggestion(s) to 1 file(s) --- messages/uniworx/categories/courses/courses/en-eu.msg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/uniworx/categories/courses/courses/en-eu.msg b/messages/uniworx/categories/courses/courses/en-eu.msg index 96c84588f..da740b3ae 100644 --- a/messages/uniworx/categories/courses/courses/en-eu.msg +++ b/messages/uniworx/categories/courses/courses/en-eu.msg @@ -277,4 +277,4 @@ LecturerInvitationAccepted lType csh: You were registered as #{lType} for #{csh} CourseExamRegistrationTime: Registered since CourseParticipantStateIsActiveFilter: View CourseApply: Apply for course -CourseAdministrator: Administrator \ No newline at end of file +CourseAdministrator: Course administrator \ No newline at end of file From e2d43fd0dea3a8b150413856787f6b43f5ead40b Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 12 Aug 2021 12:03:04 +0200 Subject: [PATCH 007/143] Apply 1 suggestion(s) to 1 file(s) --- messages/uniworx/categories/courses/courses/de-de-formal.msg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/uniworx/categories/courses/courses/de-de-formal.msg b/messages/uniworx/categories/courses/courses/de-de-formal.msg index cb324e1dc..2334163bc 100644 --- a/messages/uniworx/categories/courses/courses/de-de-formal.msg +++ b/messages/uniworx/categories/courses/courses/de-de-formal.msg @@ -278,4 +278,4 @@ LecturerInvitationAccepted lType@Text csh@CourseShorthand: Sie wurden als #{lTyp CourseExamRegistrationTime: Angemeldet seit CourseParticipantStateIsActiveFilter: Ansicht CourseApply: Zum Kurs bewerben -CourseAdministrator: Administrator:in \ No newline at end of file +CourseAdministrator: Kursadministrator:in \ No newline at end of file From 0524f0a1203f70b6943e256759b9a70d452054c3 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 12 Aug 2021 12:04:02 +0200 Subject: [PATCH 008/143] Apply 1 suggestion(s) to 1 file(s) --- messages/uniworx/categories/jobs_handler/en-eu.msg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/uniworx/categories/jobs_handler/en-eu.msg b/messages/uniworx/categories/jobs_handler/en-eu.msg index be3093426..d98247824 100644 --- a/messages/uniworx/categories/jobs_handler/en-eu.msg +++ b/messages/uniworx/categories/jobs_handler/en-eu.msg @@ -17,4 +17,4 @@ InvitationAcceptDecline: Accept/Decline invitation InvitationFromTip displayName: You are receiving this invitation because #{displayName} has caused it to be sent from within Uni2work. InvitationFromTipAnonymous: You are receiving this invitiation because an user who didn't log in has caused it to be send from within Uni2work. InvitationUniWorXTip: Uni2work is a web based teaching management system at LMU Munich. -LinkActiveUntil time@Text: The link is only available until #{time}! \ No newline at end of file +LinkActiveUntil time@Text: The link is only active until #{time}! \ No newline at end of file From 85006ff389188b56a8b61943621c190c9a9503b7 Mon Sep 17 00:00:00 2001 From: ros Date: Thu, 12 Aug 2021 12:06:49 +0200 Subject: [PATCH 009/143] feat(link password time): restore application --- src/Application.hs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Application.hs b/src/Application.hs index 001d87096..bcaf1edda 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -17,10 +17,9 @@ module Application ) where import Control.Monad.Logger (liftLoc, LoggingT(..), MonadLoggerIO(..)) -import Database.Persist.Postgresql ( openSimpleConn, pgConnStr, pgPoolIdleTimeout +import Database.Persist.Postgresql ( openSimpleConn, pgConnStr, connClose, pgPoolIdleTimeout , pgPoolSize ) -import Database.Persist.SqlBackend.Internal ( connClose ) import qualified Database.PostgreSQL.Simple as PG import Import hiding (cancel, respond) import Language.Haskell.TH.Syntax (qLocation) @@ -143,7 +142,6 @@ import Handler.Participants import Handler.StorageKey import Handler.Workflow import Handler.Error -import Handler.Upload -- This line actually creates our YesodDispatch instance. It is the second half @@ -709,4 +707,4 @@ addPWEntry :: User addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do PWHashConf{..} <- getsYesod $ view _appAuthPWHash (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength - void $ insert User{..} \ No newline at end of file + void $ insert User{..} From abdc2a8926ae374f5ebd9d03c9ff995b1e1b0b76 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Mon, 16 Aug 2021 15:04:12 +0200 Subject: [PATCH 010/143] refactor(corrections-r): modernize --- package.yaml | 1 + src/Handler/Course/User.hs | 3 +- src/Handler/Submission/Grade.hs | 3 +- src/Handler/Submission/List.hs | 567 +++++++++++++------------- src/Handler/Utils/Table/Pagination.hs | 27 +- src/Import/NoModel.hs | 3 + 6 files changed, 312 insertions(+), 292 deletions(-) diff --git a/package.yaml b/package.yaml index 9b8fdec52..a2afdc4f7 100644 --- a/package.yaml +++ b/package.yaml @@ -121,6 +121,7 @@ dependencies: - http-types - jose-jwt - mono-traversable + - mono-traversable-keys - lens-aeson - systemd - streaming-commons diff --git a/src/Handler/Course/User.hs b/src/Handler/Course/User.hs index 30ef678c2..65db94981 100644 --- a/src/Handler/Course/User.hs +++ b/src/Handler/Course/User.hs @@ -243,7 +243,8 @@ courseUserSubmissionsSection :: Entity Course -> Entity User -> MaybeT Handler W courseUserSubmissionsSection (Entity cid Course{..}) (Entity uid _) = do guardM . lift . hasWriteAccessTo $ CourseR courseTerm courseSchool courseShorthand CCorrectionsR - let whereClause = (E.&&.) <$> courseIs cid <*> userIs uid + let whereClause :: CorrectionTableWhere + whereClause = (E.&&.) <$> courseIs cid <*> userIs uid colonnade = mconcat -- should match getSSubsR for consistent UX [ colSelect , colSheet diff --git a/src/Handler/Submission/Grade.hs b/src/Handler/Submission/Grade.hs index 88b181f50..2ee574b5b 100644 --- a/src/Handler/Submission/Grade.hs +++ b/src/Handler/Submission/Grade.hs @@ -19,7 +19,8 @@ getCorrectionsGradeR, postCorrectionsGradeR :: Handler Html getCorrectionsGradeR = postCorrectionsGradeR postCorrectionsGradeR = do uid <- requireAuthId - let whereClause = ratedBy uid + let whereClause :: CorrectionTableWhere + whereClause = ratedBy uid displayColumns = mconcat -- should match getSSubsR for consistent UX [ -- dbRow, colSchool diff --git a/src/Handler/Submission/List.hs b/src/Handler/Submission/List.hs index 345cadd99..b8151d625 100644 --- a/src/Handler/Submission/List.hs +++ b/src/Handler/Submission/List.hs @@ -1,3 +1,6 @@ +{-# OPTIONS_GHC -fno-warn-redundant-constraints #-} +{-# OPTIONS_GHC -fno-warn-unused-top-binds #-} + module Handler.Submission.List ( getCorrectionsR, postCorrectionsR , getCCorrectionsR, postCCorrectionsR @@ -7,7 +10,7 @@ module Handler.Submission.List , ratedBy, courseIs, sheetIs, userIs , colTerm, colSchool, colCourse, colSheet, colCorrector, colSubmissionLink, colSelect, colSubmittors, colSMatrikel, colRating, colAssigned, colRated, colPseudonyms, colRatedField, colPointsField, colMaxPointsField, colCommentField, colLastEdit, colSGroups , makeCorrectionsTable - , CorrectionTableData + , CorrectionTableData, CorrectionTableWhere , ActionCorrections(..), downloadAction, deleteAction, assignAction, autoAssignAction ) where @@ -28,7 +31,6 @@ import qualified Data.CaseInsensitive as CI import Database.Esqueleto.Utils.TH import qualified Database.Esqueleto.Legacy as E import qualified Database.Esqueleto.Utils as E -import qualified Database.Esqueleto.Internal.Internal as IE (From) import Text.Hamlet (ihamletFile) @@ -40,7 +42,7 @@ import Data.List (genericLength) newtype CorrectionTableFilterProj = CorrectionTableFilterProj { corrProjFilterSubmission :: Maybe (Set [CI Char]) } - + instance Default CorrectionTableFilterProj where def = CorrectionTableFilterProj { corrProjFilterSubmission = Nothing @@ -48,194 +50,270 @@ instance Default CorrectionTableFilterProj where makeLenses_ ''CorrectionTableFilterProj -type CorrectionTableExpr = (E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity Submission)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User)) -type CorrectionTableWhere = CorrectionTableExpr -> E.SqlExpr (E.Value Bool) -type CorrectionTableData = DBRow (Entity Submission, Entity Sheet, (CourseName, CourseShorthand, Key Term, Key School), Maybe (Entity User), Maybe UTCTime, Map UserId (User, Maybe Pseudonym, Maybe SubmissionGroupName), CryptoFileNameSubmission, Bool {- Access to non-anonymous submission data -}) -correctionsTableQuery :: CorrectionTableWhere -> (CorrectionTableExpr -> v) -> CorrectionTableExpr -> E.SqlQuery v -correctionsTableQuery whereClause returnStatement t@((course `E.InnerJoin` sheet `E.InnerJoin` submission) `E.LeftOuterJoin` corrector) = do - E.on $ corrector E.?. UserId E.==. submission E.^. SubmissionRatingBy - E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet - E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse - E.where_ $ whereClause t - return $ returnStatement t +type CorrectionTableExpr = ( E.SqlExpr (Entity Course) + `E.InnerJoin` E.SqlExpr (Entity Sheet) + `E.InnerJoin` E.SqlExpr (Entity Submission) + ) + `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User)) +type CorrectionTableWhere = forall m. MonadReader CorrectionTableExpr m => m (E.SqlExpr (E.Value Bool)) +type CorrectionTableCourseData = (CourseName, CourseShorthand, TermId, SchoolId) +type CorrectionTableUserData = (User, Maybe Pseudonym, Maybe SubmissionGroupName) +type CorrectionTableData = DBRow ( Entity Submission + , Entity Sheet + , CorrectionTableCourseData + , Maybe (Entity User) + , Maybe UTCTime + , Map UserId CorrectionTableUserData + , CryptoFileNameSubmission + , Bool {- Access to non-anonymous submission data -} + ) -lastEditQuery :: IE.From (E.SqlExpr (Entity SubmissionEdit)) - => E.SqlExpr (Entity Submission) -> E.SqlExpr (E.Value (Maybe UTCTime)) -lastEditQuery submission = E.subSelectMaybe $ E.from $ \edit -> do - E.where_ $ edit E.^. SubmissionEditSubmission E.==. submission E.^. SubmissionId - return $ E.max_ $ edit E.^. SubmissionEditTime -queryCourse :: CorrectionTableExpr -> E.SqlExpr (Entity Course) -queryCourse = $(sqlIJproj 3 1) . $(sqlLOJproj 2 1) +queryCourse :: Getter CorrectionTableExpr (E.SqlExpr (Entity Course)) +queryCourse = to $ $(sqlIJproj 3 1) . $(sqlLOJproj 2 1) -querySubmission :: CorrectionTableExpr -> E.SqlExpr (Entity Submission) -querySubmission = $(sqlIJproj 3 3) . $(sqlLOJproj 2 1) +querySheet :: Getter CorrectionTableExpr (E.SqlExpr (Entity Sheet)) +querySheet = to $ $(sqlIJproj 3 2) . $(sqlLOJproj 2 1) + +querySubmission :: Getter CorrectionTableExpr (E.SqlExpr (Entity Submission)) +querySubmission = to $ $(sqlIJproj 3 3) . $(sqlLOJproj 2 1) + +queryCorrector :: Getter CorrectionTableExpr (E.SqlExpr (Maybe (Entity User))) +queryCorrector = to $(sqlLOJproj 2 2) + +queryLastEdit :: Getter CorrectionTableExpr (E.SqlExpr (E.Value (Maybe UTCTime))) +queryLastEdit = querySubmission . submissionLastEdit + where + submissionLastEdit = to $ \submission -> E.subSelectMaybe . E.from $ \edit -> do + E.where_ $ edit E.^. SubmissionEditSubmission E.==. submission E.^. SubmissionId + return $ E.max_ $ edit E.^. SubmissionEditTime + + +resultSubmission :: Lens' CorrectionTableData (Entity Submission) +resultSubmission = _dbrOutput . _1 + +resultSheet :: Lens' CorrectionTableData (Entity Sheet) +resultSheet = _dbrOutput . _2 + +resultCourseName :: Lens' CorrectionTableData CourseName +resultCourseName = _dbrOutput . _3 . _1 + +resultCourseShorthand :: Lens' CorrectionTableData CourseShorthand +resultCourseShorthand = _dbrOutput . _3 . _2 + +resultCourseTerm :: Lens' CorrectionTableData TermId +resultCourseTerm = _dbrOutput . _3 . _3 + +resultCourseSchool :: Lens' CorrectionTableData SchoolId +resultCourseSchool = _dbrOutput . _3 . _4 + +resultCorrector :: Traversal' CorrectionTableData (Entity User) +resultCorrector = _dbrOutput . _4 . _Just + +resultLastEdit :: Traversal' CorrectionTableData UTCTime +resultLastEdit = _dbrOutput . _5 . _Just + +resultSubmittors :: IndexedTraversal' UserId CorrectionTableData CorrectionTableUserData +resultSubmittors = _dbrOutput . _6 . itraversed + +resultUserUser :: Lens' CorrectionTableUserData User +resultUserUser = _1 + +resultUserPseudonym :: Traversal' CorrectionTableUserData Pseudonym +resultUserPseudonym = _2 . _Just + +resultUserSubmissionGroup :: Traversal' CorrectionTableUserData SubmissionGroupName +resultUserSubmissionGroup = _3 . _Just + +resultCryptoID :: Lens' CorrectionTableData CryptoFileNameSubmission +resultCryptoID = _dbrOutput . _7 + +resultNonAnonymousAccess :: Lens' CorrectionTableData Bool +resultNonAnonymousAccess = _dbrOutput . _8 -queryCorrector :: CorrectionTableExpr -> E.SqlExpr (Maybe (Entity User)) -queryCorrector = $(sqlLOJproj 2 2) -- Where Clauses ratedBy :: UserId -> CorrectionTableWhere -ratedBy uid ((_course `E.InnerJoin` _sheet `E.InnerJoin` submission) `E.LeftOuterJoin` _corrector) = submission E.^. SubmissionRatingBy E.==. E.just (E.val uid) +ratedBy uid = views querySubmission $ (E.==. E.justVal uid) . (E.^. SubmissionRatingBy) courseIs :: CourseId -> CorrectionTableWhere -courseIs cid (( course `E.InnerJoin` _sheet `E.InnerJoin` _submission) `E.LeftOuterJoin` _corrector) = course E.^. CourseId E.==. E.val cid +courseIs cid = views queryCourse $ (E.==. E.val cid) . (E.^. CourseId) sheetIs :: Key Sheet -> CorrectionTableWhere -sheetIs shid ((_course `E.InnerJoin` sheet `E.InnerJoin` _submission) `E.LeftOuterJoin` _corrector) = sheet E.^. SheetId E.==. E.val shid +sheetIs shid = views querySheet $ (E.==. E.val shid) . (E.^. SheetId) userIs :: Key User -> CorrectionTableWhere -userIs uid ((_course `E.InnerJoin` _sheet `E.InnerJoin` submission) `E.LeftOuterJoin` _corrector) = E.exists . E.from $ \submissionUser -> +userIs uid = views querySubmission $ \submission -> E.exists . E.from $ \submissionUser -> E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId E.&&. submissionUser E.^. SubmissionUserUser E.==. E.val uid + -- Columns colTerm :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colTerm = sortable (Just "term") (i18nCell MsgTableTerm) - $ \DBRow{ dbrOutput } -> - textCell $ termToText $ unTermKey $ dbrOutput ^. _3 . _3 -- kurze Semsterkürzel +colTerm = sortable (Just "term") (i18nCell MsgTableTerm) . views (resultCourseTerm . _TermId) $ textCell . termToText colSchool :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colSchool = sortable (Just "school") (i18nCell MsgTableCourseSchool) - $ \DBRow{ dbrOutput } -> let course = dbrOutput ^. _3 in - anchorCell (TermSchoolCourseListR (course ^. _3) (course ^. _4)) [whamlet|#{unSchoolKey (course ^. _4)}|] +colSchool = sortable (Just "school") (i18nCell MsgTableCourseSchool) $ \x -> + let tid = x ^. resultCourseTerm + ssh = x ^. resultCourseSchool + in anchorCell (TermSchoolCourseListR tid ssh) + (ssh ^. _SchoolId) colCourse :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colCourse = sortable (Just "course") (i18nCell MsgTableCourse) - $ \DBRow{ dbrOutput=(_, _, (_,csh,tid,sid),_ , _, _, _, _) } -> courseCellCL (tid,sid,csh) +colCourse = sortable (Just "course") (i18nCell MsgTableCourse) $ views ($(multifocusG 3) resultCourseTerm resultCourseSchool resultCourseShorthand) courseCellCL colSheet :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colSheet = sortable (Just "sheet") (i18nCell MsgTableSheet) $ \row -> - let sheet = row ^. _dbrOutput . _2 - course= row ^. _dbrOutput . _3 - tid = course ^. _3 - ssh = course ^. _4 - csh = course ^. _2 - shn = sheetName $ entityVal sheet - in anchorCell (CSheetR tid ssh csh shn SShowR) [whamlet|_{shn}|] +colSheet = sortable (Just "sheet") (i18nCell MsgTableSheet) $ \x -> + let tid = x ^. resultCourseTerm + ssh = x ^. resultCourseSchool + csh = x ^. resultCourseShorthand + shn = x ^. resultSheet . _entityVal . _sheetName + in anchorCell (CSheetR tid ssh csh shn SShowR) shn colCorrector :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colCorrector = sortable (Just "corrector") (i18nCell MsgTableCorrector) $ \case - DBRow{ dbrOutput = (_, _, _, Nothing , _, _, _, _) } -> cell mempty - DBRow{ dbrOutput = (_, _, _, Just (Entity _ User{..}), _, _, _, _) } -> userCell userDisplayName userSurname +colCorrector = sortable (Just "corrector") (i18nCell MsgTableCorrector) $ \x -> + maybeCell (x ^? resultCorrector) $ \(Entity _ User{..}) -> userCell userDisplayName userSurname colSubmissionLink :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colSubmissionLink = sortable (Just "submission") (i18nCell MsgTableSubmission) - $ \DBRow{ dbrOutput=(_, sheet, course, _, _,_, cid, _) } -> - let csh = course ^. _2 - tid = course ^. _3 - ssh = course ^. _4 - shn = sheetName $ entityVal sheet - in anchorCellC $cacheIdentHere (CSubmissionR tid ssh csh shn cid SubShowR) (toPathPiece cid) +colSubmissionLink = sortable (Just "submission") (i18nCell MsgTableSubmission) $ \x -> + let tid = x ^. resultCourseTerm + ssh = x ^. resultCourseSchool + csh = x ^. resultCourseShorthand + shn = x ^. resultSheet . _entityVal . _sheetName + subCID = x ^. resultCryptoID + in anchorCellC $cacheIdentHere (CSubmissionR tid ssh csh shn subCID SubShowR) (toPathPiece subCID) colSelect :: forall act h epId. (Semigroup act, Monoid act, Headedness h, Ord epId) => Colonnade h CorrectionTableData (DBCell _ (FormResult (act, DBFormResult CryptoFileNameSubmission Bool CorrectionTableData), SheetTypeSummary epId)) -colSelect = dbSelect (_1 . applying _2) id $ \DBRow{ dbrOutput=(_, _, _, _, _, _, cid, _) } -> return cid +colSelect = dbSelect (_1 . applying _2) id $ views resultCryptoID return + colSubmittors :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colSubmittors = sortable (Just "submittors") (i18nCell MsgSubmissionUsers) $ \DBRow{ dbrOutput=(_, _, course, _, _, users, _, hasAccess) } -> - let - csh = course ^. _2 - tid = course ^. _3 - ssh = course ^. _4 - link cid = CourseR tid ssh csh $ CUserR cid - protoCell = listCell (Map.toList users) $ \(userId, (User{..}, mPseudo, _)) -> - anchorCellCM $cacheIdentHere (link <$> encrypt userId) $ case mPseudo of - Nothing -> nameWidget userDisplayName userSurname - Just p -> [whamlet|^{nameWidget userDisplayName userSurname} (#{review _PseudonymText p})|] - in if | hasAccess -> protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] - | otherwise -> mempty +colSubmittors = sortable (Just "submittors") (i18nCell MsgSubmissionUsers) $ \x -> + let tid = x ^. resultCourseTerm + ssh = x ^. resultCourseSchool + csh = x ^. resultCourseShorthand + link uCID = CourseR tid ssh csh $ CUserR uCID + protoCell = listCell (sortOn (view $ _2 . resultUserUser . $(multifocusG 2) _userSurname _userDisplayName) $ itoListOf resultSubmittors x) $ \((encrypt -> mkUCID), u) -> + let User{..} = u ^. resultUserUser + mPseudo = u ^? resultUserPseudonym + in anchorCellCM $cacheIdentHere (link <$> mkUCID) $ + [whamlet| + $newline never + ^{nameWidget userDisplayName userSurname} + $maybe p <- mPseudo + \ (#{review _PseudonymText p}) + |] + in guardMonoid (x ^. resultNonAnonymousAccess) $ + protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] colSMatrikel :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colSMatrikel = sortable (Just "submittors-matriculation") (i18nCell MsgTableMatrikelNr) $ \DBRow{ dbrOutput=(_, _, (_, csh, tid, ssh), _, _, users, _, hasAccess) } -> - let protoCell = listCell (Map.toList $ Map.mapMaybe (\x@(User{userMatrikelnummer}, _, _) -> (x,) <$> assertM (not . null) userMatrikelnummer) users) $ \(userId, (_, matr)) -> anchorCellCM $cacheIdentHere (CourseR tid ssh csh . CUserR <$> encrypt userId) matr - in if | hasAccess -> protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] - | otherwise -> mempty +colSMatrikel = sortable (Just "submittors-matriculation") (i18nCell MsgTableMatrikelNr) $ \x -> + let protoCell = listCell (sort $ x ^.. resultSubmittors . resultUserUser . _userMatrikelnummer . _Just) wgtCell + in guardMonoid (x ^. resultNonAnonymousAccess) $ + protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] colSGroups :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colSGroups = sortable (Just "submittors-group") (i18nCell MsgTableSubmissionGroup) $ \DBRow{ dbrOutput=(_, Entity _ Sheet{..}, _, _, _, users, _, hasAccess) } -> - let protoCell = listCell (nubOrdOn (view _2) . Map.toList $ Map.mapMaybe (view _3) users) $ \(_, sGroup) -> cell $ toWidget sGroup - in if | hasAccess - , is _RegisteredGroups sheetGrouping - -> protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] - | otherwise - -> mempty +colSGroups = sortable (Just "submittors-group") (i18nCell MsgTableSubmissionGroup) $ \x -> + let protoCell = listCell (setOf (resultSubmittors . resultUserSubmissionGroup) x) wgtCell + in guardMonoid (x ^. resultNonAnonymousAccess) $ + protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] -colRating :: forall m a. IsDBTable m (a, SheetTypeSummary SqlBackendKey) => Colonnade Sortable CorrectionTableData (DBCell m (a, SheetTypeSummary SqlBackendKey)) -colRating = sortable (Just "rating") (i18nCell MsgTableRating) $ \DBRow{ dbrOutput=(Entity subId sub@Submission{..}, Entity _ Sheet{..}, course, _, _, _, _, _) } -> - let csh = course ^. _2 - tid = course ^. _3 - ssh = course ^. _4 - -- shn = sheetName +colRating :: forall m a a'. (IsDBTable m a, a ~ (a', SheetTypeSummary SqlBackendKey)) => Colonnade Sortable CorrectionTableData (DBCell m a) +colRating = colRating' _2 - mkRoute = do - cid <- encrypt subId - return $ CSubmissionR tid ssh csh sheetName cid CorrectionR - in mconcat - [ anchorCellCM $cacheIdentHere mkRoute $(widgetFile "widgets/rating/rating") - , writerCell $ do - let - summary :: SheetTypeSummary SqlBackendKey - summary = sheetTypeSum sheetType $ submissionRatingPoints <* guard (submissionRatingDone sub) - scribe (_2 :: Lens' (a, SheetTypeSummary SqlBackendKey) (SheetTypeSummary SqlBackendKey)) summary - ] +colRating' :: forall m a. IsDBTable m a => ASetter' a (SheetTypeSummary SqlBackendKey) -> Colonnade Sortable CorrectionTableData (DBCell m a) +colRating' l = sortable (Just "rating") (i18nCell MsgTableRating) $ \x -> + let tid = x ^. resultCourseTerm + ssh = x ^. resultCourseSchool + csh = x ^. resultCourseShorthand + shn = x ^. resultSheet . _entityVal . _sheetName + cID = x ^. resultCryptoID + sub@Submission{..} = x ^. resultSubmission . _entityVal + Sheet{..} = x ^. resultSheet . _entityVal + + mkRoute = return $ CSubmissionR tid ssh csh shn cID CorrectionR + in mconcat + [ anchorCellCM $cacheIdentHere mkRoute $(widgetFile "widgets/rating/rating") + , writerCell $ do + let summary :: SheetTypeSummary SqlBackendKey + summary = sheetTypeSum sheetType $ submissionRatingPoints <* guard (submissionRatingDone sub) + scribe l summary + ] colAssigned :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colAssigned = sortable (Just "assignedtime") (i18nCell MsgAssignedTime) $ \DBRow{ dbrOutput=(Entity _subId Submission{..}, _sheet, _course, _, _, _, _, _) } -> - maybe mempty dateTimeCell submissionRatingAssigned +colAssigned = sortable (Just "assignedtime") (i18nCell MsgAssignedTime) $ \x -> maybeCell (x ^? resultSubmission . _entityVal . _submissionRatingAssigned . _Just) dateTimeCell colRated :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colRated = sortable (Just "ratingtime") (i18nCell MsgTableRatingTime) $ \DBRow{ dbrOutput=(Entity _subId Submission{..}, _sheet, _course, _, _, _, _, _) } -> - maybe mempty dateTimeCell submissionRatingTime +colRated = sortable (Just "ratingtime") (i18nCell MsgTableRatingTime) $ \x -> maybeCell (x ^? resultSubmission . _entityVal . _submissionRatingTime . _Just) dateTimeCell colPseudonyms :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colPseudonyms = sortable Nothing (i18nCell MsgPseudonyms) $ \DBRow{ dbrOutput=(_, _, _, _, _, users, _, _) } -> let - lCell = listCell (catMaybes $ view (_2 . _2) <$> Map.toList users) $ \pseudo -> - cell [whamlet|#{review _PseudonymText pseudo}|] - in lCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] +colPseudonyms = sortable Nothing (i18nCell MsgPseudonyms) $ \x -> + let protoCell = listCell (sort $ x ^.. resultSubmittors . resultUserPseudonym . re _PseudonymText) wgtCell + in protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] -colRatedField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (Bool, a, b) CorrectionTableData))) -colRatedField = sortable Nothing (i18nCell MsgRatingDone) $ formCell id - (\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _, _, _) } -> return subId) - (\DBRow{ dbrOutput=(Entity _ (submissionRatingDone -> done), _, _, _, _, _, _, _) } mkUnique -> over (_1.mapped) (_1 .~) . over _2 fvWidget <$> mreq checkBoxField (fsUniq mkUnique "rated") (Just done)) +colRatedField :: a' ~ (Bool, a, b) => Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId a' CorrectionTableData))) +colRatedField = colRatedField' _1 -colPointsField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, Maybe Points, b) CorrectionTableData))) -colPointsField = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ formCell id - (\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _, _, _) } -> return subId) - (\DBRow{ dbrOutput=(Entity _ Submission{..}, Entity _ Sheet{..}, _, _, _, _, _, _) } mkUnique -> case sheetType of - NotGraded -> pure $ over (_1.mapped) (_2 .~) (FormSuccess Nothing, mempty) - _other -> over (_1.mapped) (_2 .~) . over _2 fvWidget <$> mopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType) (fsUniq mkUnique "points") (Just submissionRatingPoints) +colRatedField' :: ASetter' a Bool -> Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId a CorrectionTableData))) +colRatedField' l = sortable Nothing (i18nCell MsgRatingDone) $ formCell id + (views (resultSubmission . _entityKey) return) + (\(views (resultSubmission . _entityVal) submissionRatingDone -> done) mkUnique -> over (_1.mapped) (l .~) . over _2 fvWidget <$> mreq checkBoxField (fsUniq mkUnique "rated") (Just done)) + +colPointsField :: a' ~ (a, Maybe Points, b) => Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId a' CorrectionTableData))) +colPointsField = colPointsField' _2 + +colPointsField' :: ASetter' a (Maybe Points) -> Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId a CorrectionTableData))) +colPointsField' l = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ formCell id + (views (resultSubmission . _entityKey) return) + (\(view $ $(multifocusG 2) (resultSubmission . _entityVal) (resultSheet . _entityVal) -> (Submission{..}, Sheet{..})) mkUnique -> case sheetType of + NotGraded -> pure $ over (_1.mapped) (l .~) (FormSuccess Nothing, mempty) + _other -> over (_1.mapped) (l .~) . over _2 fvWidget <$> mopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType) (fsUniq mkUnique "points") (Just submissionRatingPoints) ) -colMaxPointsField :: _ => Colonnade Sortable CorrectionTableData (DBCell m (FormResult (DBFormResult SubmissionId (a, Maybe Points, b) CorrectionTableData))) -colMaxPointsField = sortable (Just "sheet-type") (i18nCell MsgTableSheetType) $ \DBRow{ dbrOutput=(_, Entity _ Sheet{sheetCourse, sheetType}, _, _, _, _, _, _) } -> cell $ do +colMaxPointsField :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) +colMaxPointsField = sortable (Just "sheet-type") (i18nCell MsgTableSheetType) $ \x -> cell $ do + let Sheet{..} = x ^. resultSheet . _entityVal sheetTypeDesc <- liftHandler . runDB $ sheetTypeDescription sheetCourse sheetType - tr <- getTranslate - toWidget $ sheetTypeDesc tr + toWidget . sheetTypeDesc =<< getTranslate -colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData))) -colCommentField = sortable (Just "comment") (i18nCell MsgRatingComment) $ (cellAttrs <>~ [("style","width:60%")]) <$> formCell id - (\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _, _, _) } -> return subId) - (\DBRow{ dbrOutput=(Entity _ Submission{..}, _, _, _, _, _, _, _) } mkUnique -> over (_1.mapped) ((_3 .~) . assertM (not . null) . fmap (Text.strip . unTextarea)) . over _2 fvWidget <$> mopt textareaField (fsUniq mkUnique "comment") (Just $ Textarea <$> submissionRatingComment)) +colCommentField :: a' ~ (a, b, Maybe Text) => Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId a' CorrectionTableData))) +colCommentField = colCommentField' _3 + +colCommentField' :: ASetter' a (Maybe Text) -> Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId a CorrectionTableData))) +colCommentField' l = sortable (Just "comment") (i18nCell MsgRatingComment) $ (cellAttrs <>~ [("style","width:60%")]) <$> formCell id + (views (resultSubmission . _entityKey) return) + (\(view (resultSubmission . _entityVal) -> Submission{..}) mkUnique -> over (_1.mapped) ((l .~) . assertM (not . null) . fmap (Text.strip . unTextarea)) . over _2 fvWidget <$> mopt textareaField (fsUniq mkUnique "comment") (Just $ Textarea <$> submissionRatingComment)) colLastEdit :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colLastEdit = sortable (Just "last-edit") (i18nCell MsgTableLastEdit) $ - \DBRow{ dbrOutput=(_, _, _, _, mbLastEdit, _, _, _) } -> maybe mempty dateTimeCell mbLastEdit +colLastEdit = sortable (Just "last-edit") (i18nCell MsgTableLastEdit) $ \x -> maybeCell (x ^? resultLastEdit) dateTimeCell makeCorrectionsTable :: ( IsDBTable m x, ToSortable h, Functor h ) => CorrectionTableWhere -> Colonnade h CorrectionTableData (DBCell m x) -> _ -> PSValidator m x -> DBParams m x -> DB (DBResult m x) makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtParams = do - let dbtSQLQuery :: CorrectionTableExpr -> E.SqlQuery _ - dbtSQLQuery = correctionsTableQuery whereClause - (\((course `E.InnerJoin` sheet `E.InnerJoin` submission) `E.LeftOuterJoin` corrector) -> - let crse = ( course E.^. CourseName :: E.SqlExpr (E.Value CourseName) - , course E.^. CourseShorthand - , course E.^. CourseTerm - , course E.^. CourseSchool :: E.SqlExpr (E.Value SchoolId) - ) - in (submission, sheet, crse, corrector, lastEditQuery submission) - ) + let dbtSQLQuery = runReaderT $ do + course <- view queryCourse + sheet <- view querySheet + submission <- view querySubmission + corrector <- view queryCorrector + + lift $ do + E.on $ corrector E.?. UserId E.==. submission E.^. SubmissionRatingBy + E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet + E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse + + lastEdit <- view queryLastEdit + + let crse = ( course E.^. CourseName + , course E.^. CourseShorthand + , course E.^. CourseTerm + , course E.^. CourseSchool + ) + + lift . E.where_ =<< whereClause + + return (submission, sheet, crse, corrector, lastEdit) dbtProj = (views _dbtProjRow . set _dbrOutput) =<< do (submission@(Entity sId _), sheet@(Entity shId Sheet{..}), (E.Value courseName, E.Value courseShorthand, E.Value courseTerm, E.Value courseSchool), mCorrector, E.Value mbLastEdit) <- view $ _dbtProjRow . _dbrOutput cid <- encrypt sId @@ -263,163 +341,77 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtParams (hasReadAccessTo $ CourseR courseTerm courseSchool courseShorthand CCorrectionsR) return (submission, sheet, (courseName, courseShorthand, courseTerm, courseSchool), mCorrector, mbLastEdit, submittorMap, cid, nonAnonymousAccess) + dbtRowKey = views querySubmission (E.^. SubmissionId) dbTable psValidator DBTable { dbtSQLQuery - , dbtRowKey = \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) -> submission E.^. SubmissionId + , dbtRowKey , dbtColonnade , dbtProj - , dbtSorting = Map.fromList - [ ( "term" - , SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseTerm - ) - , ( "school" - , SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseSchool - ) - , ( "course" - , SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseShorthand - ) - , ( "sheet" - , SortColumn $ \((_ `E.InnerJoin` sheet `E.InnerJoin` _) `E.LeftOuterJoin` _) -> sheet E.^. SheetName - ) - , ( "corrector" - , SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` corrector) -> corrector E.?. UserSurname - ) - , ( "rating" - , SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> submission E.^. SubmissionRatingPoints - ) - , ( "sheet-type" - , SortColumns $ \((_ `E.InnerJoin` sheet `E.InnerJoin` _) `E.LeftOuterJoin` _) -> + , dbtSorting = mconcat + [ singletonMap "term" . SortColumn $ views queryCourse (E.^. CourseTerm) + , singletonMap "school" . SortColumn $ views queryCourse (E.^. CourseSchool) + , singletonMap "course" . SortColumn $ views queryCourse (E.^. CourseShorthand) + , singletonMap "sheet" . SortColumn $ views querySheet (E.^. SheetName) + , singletonMap "corrector" . SortColumns $ \x -> + [ SomeExprValue (views queryCorrector (E.?. UserSurname) x) + , SomeExprValue (views queryCorrector (E.?. UserDisplayName) x) + ] + , singletonMap "rating" . SortColumn $ views querySubmission (E.^. SubmissionRatingPoints) + , singletonMap "sheet-type" . SortColumns $ \(view querySheet -> sheet) -> [ SomeExprValue ((sheet E.^. SheetType) E.->. "type" :: E.SqlExpr (E.Value Value)) , SomeExprValue (((sheet E.^. SheetType) E.->. "grading" :: E.SqlExpr (E.Value Value)) E.->. "max" :: E.SqlExpr (E.Value Value)) , SomeExprValue (((sheet E.^. SheetType) E.->. "grading" :: E.SqlExpr (E.Value Value)) E.->. "passing" :: E.SqlExpr (E.Value Value)) ] - ) - , ( "israted" - , SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> E.not_ . E.isNothing $ submission E.^. SubmissionRatingTime - ) - , ( "ratingtime" - , SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> submission E.^. SubmissionRatingTime - ) - , ( "assignedtime" - , SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> submission E.^. SubmissionRatingAssigned - ) - , ( "submittors" - , SortProjected . comparing $ \DBRow{ dbrOutput = (_, _, _, _, _, submittors, _, hasAccess) } -> guardOn @Maybe hasAccess . fmap ((userSurname &&& userDisplayName) . view _1) $ Map.elems submittors - ) - , ( "submittors-matriculation" - , SortProjected . comparing $ \DBRow{ dbrOutput = (_, _, _, _, _, submittors, _, hasAccess) } -> guardOn @Maybe hasAccess . fmap (view $ _1 . _userMatrikelnummer) $ Map.elems submittors - ) - , ( "submittors-group" - , SortProjected . comparing $ \DBRow{ dbrOutput = (_, _, _, _, _, submittors, _, hasAccess) } -> guardOn @Maybe hasAccess . fmap (view _3) $ Map.elems submittors - ) - , ( "comment" -- sorting by comment specifically requested by correctors to easily see submissions to be done - , SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> submission E.^. SubmissionRatingComment - ) - , ( "last-edit" - , SortColumn $ \((_course `E.InnerJoin` _sheet `E.InnerJoin` submission) `E.LeftOuterJoin` _corrector) -> lastEditQuery submission - ) - , ( "submission" - , SortProjected . comparing $ toPathPiece . view (_dbrOutput . _7) - ) + , singletonMap "israted" . SortColumn $ views querySubmission $ E.not_ . E.isNothing . (E.^. SubmissionRatingTime) + , singletonMap "ratingtime" . SortColumn $ views querySubmission (E.^. SubmissionRatingTime) + , singletonMap "assignedtime" . SortColumn $ views querySubmission (E.^. SubmissionRatingAssigned) + , singletonMap "submittors" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserUser . $(multifocusG 2) _userSurname _userDisplayName) x + , singletonMap "submittors-matriculation" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserUser . _userMatrikelnummer . _Just) x + , singletonMap "submittors-group" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserSubmissionGroup) x + , singletonMap "comment" . SortColumn $ views querySubmission (E.^. SubmissionRatingComment) -- sorting by comment specifically requested by correctors to easily see submissions to be done + , singletonMap "last-edit" . SortColumn $ view queryLastEdit + , singletonMap "submission" . SortProjected . comparing $ toPathPiece . view resultCryptoID ] - , dbtFilter = Map.fromList - [ ( "term" - , FilterColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _ :: CorrectionTableExpr) tids -> if - | Set.null tids -> E.val True :: E.SqlExpr (E.Value Bool) - | otherwise -> course E.^. CourseTerm `E.in_` E.valList (Set.toList tids) - ) - , ( "school" - , FilterColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _ :: CorrectionTableExpr) sids -> if - | Set.null sids -> E.val True :: E.SqlExpr (E.Value Bool) - | otherwise -> course E.^. CourseSchool `E.in_` E.valList (Set.toList sids) - ) - , ( "course" - , FilterColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _ :: CorrectionTableExpr) cshs -> if - | Set.null cshs -> E.val True :: E.SqlExpr (E.Value Bool) - | otherwise -> course E.^. CourseShorthand `E.in_` E.valList (Set.toList cshs) - ) - , ( "sheet" - , FilterColumn $ \((_ `E.InnerJoin` sheet `E.InnerJoin` _) `E.LeftOuterJoin` _ :: CorrectionTableExpr) shns -> if - | Set.null shns -> E.val True :: E.SqlExpr (E.Value Bool) - | otherwise -> sheet E.^. SheetName `E.in_` E.valList (Set.toList shns) - ) - , ( "sheet-search" - , FilterColumn $ \((_ `E.InnerJoin` sheet `E.InnerJoin` _) `E.LeftOuterJoin` _ :: CorrectionTableExpr) shns -> case getLast (shns :: Last (CI Text)) of - Nothing -> E.val True :: E.SqlExpr (E.Value Bool) - Just needle -> sheet E.^. SheetName `E.ilike` (E.%) E.++. E.val needle E.++. (E.%) - ) - , ( "corrector" - , FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` corrector :: CorrectionTableExpr) emails -> if - | Set.null emails -> E.val True :: E.SqlExpr (E.Value Bool) - | otherwise -> corrector E.?. UserEmail `E.in_` E.justList (E.valList . catMaybes $ Set.toList emails) - E.||. (if Nothing `Set.member` emails then E.isNothing (corrector E.?. UserEmail) else E.val False) - ) - , ( "isassigned" - , FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) criterion -> case getLast (criterion :: Last Bool) of - Nothing -> E.val True :: E.SqlExpr (E.Value Bool) - Just True -> E.isJust $ submission E.^. SubmissionRatingBy - Just False-> E.isNothing $ submission E.^. SubmissionRatingBy - ) - , ( "israted" - , FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) criterion -> case getLast (criterion :: Last Bool) of - Nothing -> E.val True :: E.SqlExpr (E.Value Bool) - Just True -> E.isJust $ submission E.^. SubmissionRatingTime - Just False-> E.isNothing $ submission E.^. SubmissionRatingTime - ) - , ( "corrector-name-email" -- corrector filter does not work for text-filtering - , FilterColumn $ E.anyFilter - [ E.mkContainsFilterWith Just $ queryCorrector >>> (E.?. UserSurname) - , E.mkContainsFilterWith Just $ queryCorrector >>> (E.?. UserDisplayName) - , E.mkContainsFilterWith (Just . CI.mk) $ queryCorrector >>> (E.?. UserEmail) - ] - ) - , ( "user-name-email" - , FilterColumn $ E.mkExistsFilter $ \table needle -> E.from $ \(submissionUser `E.InnerJoin` user) -> do - E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId - E.where_ $ querySubmission table E.^. SubmissionId E.==. submissionUser E.^. SubmissionUserSubmission - E.where_ $ (\f -> f user $ Set.singleton needle) $ E.anyFilter - [ E.mkContainsFilter (E.^. UserSurname) - , E.mkContainsFilter (E.^. UserDisplayName) - , E.mkContainsFilterWith CI.mk (E.^. UserEmail) - ] - ) - , ( "user-matriclenumber" - , FilterColumn $ E.mkExistsFilter $ \table needle -> E.from $ \(submissionUser `E.InnerJoin` user) -> do - E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId - E.where_ $ querySubmission table E.^. SubmissionId E.==. submissionUser E.^. SubmissionUserSubmission - E.where_ $ (\f -> f user $ Set.singleton needle) $ - E.mkContainsFilter (E.^. UserMatrikelnummer) - ) - , ( "submission-group" - , FilterColumn $ E.mkExistsFilter $ \table needle -> E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser) -> do - E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup - E.where_ $ queryCourse table E.^. CourseId E.==. submissionGroup E.^. SubmissionGroupCourse - E.where_ $ (\f -> f submissionGroup $ Set.singleton needle) $ - E.mkContainsFilter (E.^. SubmissionGroupName) - ) - , ( "rating-visible" - , FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) criterion -> case getLast (criterion :: Last Bool) of - Nothing -> E.val True :: E.SqlExpr (E.Value Bool) - Just True -> E.isJust $ submission E.^. SubmissionRatingTime - Just False-> E.isNothing $ submission E.^. SubmissionRatingTime - ) - , ( "rating" - , FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) pts -> if - | Set.null pts -> E.val True :: E.SqlExpr (E.Value Bool) - | otherwise -> E.maybe (E.val False :: E.SqlExpr (E.Value Bool)) (\p -> p `E.in_` E.valList (Set.toList pts)) (submission E.^. SubmissionRatingPoints) - ) - , ( "comment" - , FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) comm -> case getLast (comm :: Last Text) of - Nothing -> E.val True :: E.SqlExpr (E.Value Bool) - Just needle -> E.maybe (E.val False :: E.SqlExpr (E.Value Bool)) (E.isInfixOf $ E.val needle) (submission E.^. SubmissionRatingComment) - ) - , ( "submission" - , FilterProjected (_corrProjFilterSubmission ?~) - -- , FilterProjected $ \(DBRow{..} :: CorrectionTableData) (criteria :: Set Text) -> - -- let cid = map CI.mk . unpack . toPathPiece $ dbrOutput ^. _7 - -- criteria' = map CI.mk . unpack <$> Set.toList criteria - -- in any (`isInfixOf` cid) criteria' - ) + , dbtFilter = mconcat + [ singletonMap "term" . FilterColumn . E.mkExactFilter $ views queryCourse (E.^. CourseTerm) + , singletonMap "school" . FilterColumn . E.mkExactFilter $ views queryCourse (E.^. CourseSchool) + , singletonMap "course" . FilterColumn . E.mkExactFilter $ views queryCourse (E.^. CourseShorthand) + , singletonMap "sheet" . FilterColumn . E.mkExactFilter $ views querySheet (E.^. SheetName) + , singletonMap "sheet-search" . FilterColumn . E.mkContainsFilter $ views querySheet (E.^. SheetName) + , singletonMap "corrector" . FilterColumn . E.mkExactFilterWith Just $ views queryCorrector (E.?. UserIdent) + , singletonMap "isassigned" . FilterColumn . E.mkExactFilterLast $ views querySubmission (E.isJust . (E.^. SubmissionRatingBy)) + , singletonMap "israted" . FilterColumn . E.mkExactFilterLast $ views querySubmission sqlSubmissionRatingDone + , singletonMap "corrector-name-email" . FilterColumn $ E.anyFilter + [ E.mkContainsFilterWith Just $ views queryCorrector (E.?. UserSurname) + , E.mkContainsFilterWith Just $ views queryCorrector (E.?. UserDisplayName) + , E.mkContainsFilterWith (Just . CI.mk) $ views queryCorrector (E.?. UserEmail) + , E.mkContainsFilterWith (Just . CI.mk) $ views queryCorrector (E.?. UserIdent) + , E.mkContainsFilterWith (Just . CI.mk) $ views queryCorrector (E.?. UserDisplayEmail) + ] + , singletonMap "user-name-email" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionUser `E.InnerJoin` user) -> do + E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId + E.where_ $ dbtRowKey row E.==. submissionUser E.^. SubmissionUserSubmission + E.where_ $ E.anyFilter + [ E.mkContainsFilter (E.^. UserSurname) + , E.mkContainsFilter (E.^. UserDisplayName) + , E.mkContainsFilterWith CI.mk (E.^. UserEmail) + , E.mkContainsFilterWith CI.mk (E.^. UserIdent) + , E.mkContainsFilterWith CI.mk (E.^. UserDisplayEmail) + ] user (Set.singleton needle) + , singletonMap "user-matriclenumber" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionUser `E.InnerJoin` user) -> do + E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId + E.where_ $ dbtRowKey row E.==. submissionUser E.^. SubmissionUserSubmission + E.where_ $ E.mkContainsFilter (E.^. UserMatrikelnummer) user (Set.singleton needle) + , singletonMap "submission-group" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser `E.InnerJoin` submissionUser) -> do + E.on $ submissionUser E.^. SubmissionUserUser E.==. submissionGroupUser E.^. SubmissionGroupUserUser + E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup + E.where_ $ (row ^. queryCourse) E.^. CourseId E.==. submissionGroup E.^. SubmissionGroupCourse + E.&&. dbtRowKey row E.==. submissionUser E.^. SubmissionUserSubmission + E.where_ $ E.mkContainsFilter (E.^. SubmissionGroupName) submissionGroup (Set.singleton needle) + , singletonMap "rating-visible" . FilterColumn . E.mkExactFilterLast $ views querySubmission sqlSubmissionRatingDone -- TODO: Identical with israted? + , singletonMap "rating" . FilterColumn . E.mkExactFilterWith Just $ views querySubmission (E.^. SubmissionRatingPoints) + , singletonMap "comment" . FilterColumn . E.mkContainsFilterWith Just $ views querySubmission (E.^. SubmissionRatingComment) + , singletonMap "submission" $ FilterProjected (_corrProjFilterSubmission ?~) ] , dbtFilterUI = fromMaybe mempty dbtFilterUI , dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (const defaultDBSFilterLayout) dbtFilterUI } @@ -447,7 +439,7 @@ data ActionCorrectionsData = CorrDownloadData SubmissionDownloadAnonymous Submis | CorrAutoSetCorrectorData SheetId | CorrDeleteData -correctionsR :: _ -> _ -> _ -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler TypedContent +correctionsR :: CorrectionTableWhere -> _ -> _ -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler TypedContent correctionsR whereClause displayColumns dbtFilterUI psValidator actions = do (table, statistics) <- correctionsR' whereClause displayColumns dbtFilterUI psValidator actions @@ -455,7 +447,7 @@ correctionsR whereClause displayColumns dbtFilterUI psValidator actions = do setTitleI MsgCourseCorrectionsTitle $(widgetFile "corrections") -correctionsR' :: _ -> _ -> _ -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler (Widget, SheetTypeSummary SqlBackendKey) +correctionsR' :: CorrectionTableWhere -> _ -> _ -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler (Widget, SheetTypeSummary SqlBackendKey) correctionsR' whereClause displayColumns dbtFilterUI psValidator actions = do currentRoute <- fromMaybe (error "correctionsR called from 404-handler") <$> getCurrentRoute -- This should never be called from a 404 handler @@ -654,7 +646,8 @@ getCorrectionsR, postCorrectionsR :: Handler TypedContent getCorrectionsR = postCorrectionsR postCorrectionsR = do uid <- requireAuthId - let whereClause = ratedBy uid + let whereClause :: CorrectionTableWhere + whereClause = ratedBy uid colonnade = mconcat [ colSelect , colSchool @@ -701,7 +694,8 @@ getCCorrectionsR, postCCorrectionsR :: TermId -> SchoolId -> CourseShorthand -> getCCorrectionsR = postCCorrectionsR postCCorrectionsR tid ssh csh = do Entity cid _ <- runDB $ getBy404 $ TermSchoolCourseShort tid ssh csh - let whereClause = courseIs cid + let whereClause :: CorrectionTableWhere + whereClause = courseIs cid colonnade = mconcat -- should match getSSubsR for consistent UX [ colSelect , colSheet @@ -737,7 +731,8 @@ getSSubsR, postSSubsR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> H getSSubsR = postSSubsR postSSubsR tid ssh csh shn = do shid <- runDB $ fetchSheetId tid ssh csh shn - let whereClause = sheetIs shid + let whereClause :: CorrectionTableWhere + whereClause = sheetIs shid colonnade = mconcat -- should match getCCorrectionsR for consistent UX [ colSelect , colSMatrikel diff --git a/src/Handler/Utils/Table/Pagination.hs b/src/Handler/Utils/Table/Pagination.hs index a3471822d..18203825d 100644 --- a/src/Handler/Utils/Table/Pagination.hs +++ b/src/Handler/Utils/Table/Pagination.hs @@ -45,7 +45,8 @@ module Handler.Utils.Table.Pagination , maybeAnchorCellM, maybeAnchorCellM', maybeLinkEitherCellM' , anchorCellC, anchorCellCM, anchorCellCM', linkEitherCellCM', maybeLinkEitherCellCM' , cellTooltip - , listCell, listCell' + , listCell, listCell', listCellOf, listCellOf' + , ilistCell, ilistCell', ilistCellOf, ilistCellOf' , formCell, DBFormResult(..), getDBFormResult , dbSelect , (&) @@ -1793,12 +1794,30 @@ listCell :: (IsDBTable m a, MonoFoldable mono) => mono -> (Element mono -> DBCel listCell = listCell' . return listCell' :: (IsDBTable m a, MonoFoldable mono) => WriterT a m mono -> (Element mono -> DBCell m a) -> DBCell m a -listCell' mkXS mkCell = review dbCell . ([], ) $ do +listCell' mkXS mkCell = ilistCell' (otoList <$> mkXS) $ const mkCell + +ilistCell :: (IsDBTable m a, MonoFoldableWithKey mono) => mono -> (MonoKey mono -> Element mono -> DBCell m a) -> DBCell m a +ilistCell = ilistCell' . return + +ilistCell' :: (IsDBTable m a, MonoFoldableWithKey mono) => WriterT a m mono -> (MonoKey mono -> Element mono -> DBCell m a) -> DBCell m a +ilistCell' mkXS mkCell = review dbCell . ([], ) $ do xs <- mkXS - cells <- forM (toList xs) $ - \(view dbCell . mkCell -> (attrs, mkWidget)) -> (attrs, ) <$> mkWidget + cells <- forM (otoKeyedList xs) $ + \(view dbCell . uncurry mkCell -> (attrs, mkWidget)) -> (attrs, ) <$> mkWidget return $(widgetFile "table/cell/list") +listCellOf :: IsDBTable m a' => Getting (Endo [a]) s a -> s -> (a -> DBCell m a') -> DBCell m a' +listCellOf l x = listCell (x ^.. l) + +listCellOf' :: IsDBTable m a' => Getting (Endo [a]) s a -> WriterT a' m s -> (a -> DBCell m a') -> DBCell m a' +listCellOf' l mkX = listCell' (toListOf l <$> mkX) + +ilistCellOf :: IsDBTable m a' => IndexedGetting i (Endo [(i, a)]) s a -> s -> (i -> a -> DBCell m a') -> DBCell m a' +ilistCellOf l x = listCell (itoListOf l x) . uncurry + +ilistCellOf' :: IsDBTable m a' => IndexedGetting i (Endo [(i, a)]) s a -> WriterT a' m s -> (i -> a -> DBCell m a') -> DBCell m a' +ilistCellOf' l mkX = listCell' (itoListOf l <$> mkX) . uncurry + newtype DBFormResult i a r = DBFormResult (Map i (r, a -> a)) instance Functor (DBFormResult i a) where diff --git a/src/Import/NoModel.hs b/src/Import/NoModel.hs index 79a6a45ca..ad0ac8f97 100644 --- a/src/Import/NoModel.hs +++ b/src/Import/NoModel.hs @@ -24,6 +24,7 @@ import ClassyPrelude.Yesod as Import , authorizationCheck , mkMessage, mkMessageFor, mkMessageVariant , YesodBreadcrumbs(..) + , MonoZip(..), ozipWith ) import UnliftIO.Async.Utils as Import @@ -235,6 +236,8 @@ import Data.Scientific as Import (Scientific, formatScientific) import Data.MultiSet as Import (MultiSet) +import Data.MonoTraversable.Keys as Import + import Control.Monad.Trans.RWS (RWST) From cb4ed8d9887e521f47689c118baf439846cd4514 Mon Sep 17 00:00:00 2001 From: ros Date: Tue, 17 Aug 2021 11:42:14 +0200 Subject: [PATCH 011/143] feat(course admin): application restore --- src/Application.hs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Application.hs b/src/Application.hs index 001d87096..7d02e6009 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -116,6 +116,8 @@ import qualified Utils.Pool as Custom import Utils.Postgresql import Handler.Utils.Memcached (manageMemcachedLocalInvalidations) +import qualified System.Clock as Clock + -- Import all relevant handler modules here. -- (HPack takes care to add new modules to our cabal file nowadays.) import Handler.News @@ -213,7 +215,7 @@ makeFoundation appSettings''@AppSettings{..} = do -- from there, and then create the real foundation. let mkFoundation :: _ -> (forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend) -> _ - mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey = UniWorX {..} + mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache = UniWorX {..} -- The UniWorX {..} syntax is an example of record wild cards. For more -- information, see: -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html @@ -233,6 +235,8 @@ makeFoundation appSettings''@AppSettings{..} = do (error "MinioConn forced in tempFoundation") (error "VerpSecret forced in tempFoundation") (error "AuthKey forced in tempFoundation") + (error "PersonalisedSheetFilesSeedKey forced in tempFoundation") + (error "VolatileClusterSettingsCache forced in tempFoundation") runAppLoggingT tempFoundation $ do $logInfoS "InstanceID" $ UUID.toText appInstanceID @@ -293,6 +297,11 @@ makeFoundation appSettings''@AppSettings{..} = do appClusterID <- clusterSetting (Proxy :: Proxy 'ClusterId) `customRunSqlPool` sqlPool appVerpSecret <- clusterSetting (Proxy :: Proxy 'ClusterVerpSecret) `customRunSqlPool` sqlPool appAuthKey <- clusterSetting (Proxy :: Proxy 'ClusterAuthKey) `customRunSqlPool` sqlPool + appPersonalisedSheetFilesSeedKey <- clusterSetting (Proxy :: Proxy 'ClusterPersonalisedSheetFilesSeedKey) `customRunSqlPool` sqlPool + + let appVolatileClusterSettingsCacheTime' = Clock.fromNanoSecs ns + where (MkFixed ns :: Nano) = realToFrac appVolatileClusterSettingsCacheTime + appVolatileClusterSettingsCache <- newTVarIO $ mkVolatileClusterSettingsCache appVolatileClusterSettingsCacheTime' needsRechunk <- exists [FileContentChunkContentBased !=. True] `customRunSqlPool` sqlPool let appSettings' = appSettings'' @@ -326,7 +335,7 @@ makeFoundation appSettings''@AppSettings{..} = do $logDebugS "Runtime configuration" $ tshow appSettings' - let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey + let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache -- Return the foundation $logDebugS "setup" "Done" @@ -709,4 +718,4 @@ addPWEntry :: User addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do PWHashConf{..} <- getsYesod $ view _appAuthPWHash (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength - void $ insert User{..} \ No newline at end of file + void $ insert User{..} From 1b6b781e82c39bc29c8984c587ac836f0da77a02 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Tue, 17 Aug 2021 11:44:14 +0200 Subject: [PATCH 012/143] fix(corrections-r): allow filtering by matriculation --- src/Handler/Course/User.hs | 17 +++-- src/Handler/Submission/Grade.hs | 18 +++--- src/Handler/Submission/List.hs | 91 +++++++++++++++++++-------- src/Handler/Utils/Table/Pagination.hs | 7 +-- 4 files changed, 86 insertions(+), 47 deletions(-) diff --git a/src/Handler/Course/User.hs b/src/Handler/Course/User.hs index 65db94981..4accc76cf 100644 --- a/src/Handler/Course/User.hs +++ b/src/Handler/Course/User.hs @@ -257,15 +257,14 @@ courseUserSubmissionsSection (Entity cid Course{..}) (Entity uid _) = do , colCorrector , colAssigned ] -- Continue here - filterUI = Just $ \mPrev -> mconcat - [ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgCourseCourseMembers) - , prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgTableMatrikelNr) - -- "pseudonym" TODO DB only stores Word24 - , Map.singleton "sheet-search" . maybeToList <$> aopt textField (fslI MsgTableSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev))) - , prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgTableCorrector) - , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableHasCorrector) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableRatingTime) - , prismAForm (singletonFilter "submission") mPrev $ aopt (lift `hoistField` textField) (fslI MsgTableSubmission) + filterUI = Just $ mconcat + [ filterUIUserNameEmail + , filterUIUserMatrikelnummer + , filterUISheetSearch + , filterUICorrectorNameEmail + , filterUIIsAssigned + , filterUIIsRated + , filterUISubmission ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway (cWdgt, statistics) <- lift . correctionsR' whereClause colonnade filterUI psValidator $ Map.fromList diff --git a/src/Handler/Submission/Grade.hs b/src/Handler/Submission/Grade.hs index 2ee574b5b..41a869ff2 100644 --- a/src/Handler/Submission/Grade.hs +++ b/src/Handler/Submission/Grade.hs @@ -38,15 +38,15 @@ postCorrectionsGradeR = do , colMaxPointsField , colCommentField ] -- Continue here - filterUI = Just $ \mPrev -> mconcat - [ prismAForm (singletonFilter "course" ) mPrev $ aopt (lift `hoistField` selectField courseOptions) (fslI MsgTableCourse) - , prismAForm (singletonFilter "term" ) mPrev $ aopt (lift `hoistField` selectField termOptions) (fslI MsgTableTerm) - , prismAForm (singletonFilter "school" ) mPrev $ aopt (lift `hoistField` selectField schoolOptions) (fslI MsgTableCourseSchool) - , Map.singleton "sheet-search" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgTableSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev))) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableRatingTime) - , prismAForm (singletonFilter "rating-visible" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingDone) - , prismAForm (singletonFilter "rating" . maybePrism _PathPiece) mPrev $ aopt (lift `hoistField` pointsField) (fslI MsgColumnRatingPoints) - , Map.singleton "comment" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgRatingComment) (Just <$> listToMaybe =<< (Map.lookup "comment" =<< mPrev)) + filterUI = Just $ mconcat + [ filterUICourse courseOptions + , filterUITerm termOptions + , filterUISchool schoolOptions + , filterUISheetSearch + , filterUIIsRated + -- , flip (prismAForm $ singletonFilter "rating-visible" . maybePrism _PathPiece) $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingDone) + , filterUIRating + , filterUIComment ] courseOptions = runDB $ do courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) diff --git a/src/Handler/Submission/List.hs b/src/Handler/Submission/List.hs index b8151d625..99b47cdda 100644 --- a/src/Handler/Submission/List.hs +++ b/src/Handler/Submission/List.hs @@ -9,6 +9,7 @@ module Handler.Submission.List , restrictAnonymous, restrictCorrector , ratedBy, courseIs, sheetIs, userIs , colTerm, colSchool, colCourse, colSheet, colCorrector, colSubmissionLink, colSelect, colSubmittors, colSMatrikel, colRating, colAssigned, colRated, colPseudonyms, colRatedField, colPointsField, colMaxPointsField, colCommentField, colLastEdit, colSGroups + , filterUICourse, filterUITerm, filterUISchool, filterUISheetSearch, filterUIIsRated, filterUISubmission, filterUIUserNameEmail, filterUIUserMatrikelnummer, filterUICorrectorNameEmail, filterUIIsAssigned, filterUISubmissionGroup, filterUIRating, filterUIComment , makeCorrectionsTable , CorrectionTableData, CorrectionTableWhere , ActionCorrections(..), downloadAction, deleteAction, assignAction, autoAssignAction @@ -289,6 +290,46 @@ colLastEdit :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m colLastEdit = sortable (Just "last-edit") (i18nCell MsgTableLastEdit) $ \x -> maybeCell (x ^? resultLastEdit) dateTimeCell +filterUICourse :: Handler (OptionList Text) -> DBFilterUI +filterUICourse courseOptions = flip (prismAForm $ singletonFilter "course") $ aopt (lift `hoistField` selectField courseOptions) (fslI MsgTableCourse) + +filterUITerm :: Handler (OptionList Text) -> DBFilterUI +filterUITerm termOptions = flip (prismAForm $ singletonFilter "term") $ aopt (lift `hoistField` selectField termOptions) (fslI MsgTableTerm) + +filterUISchool :: Handler (OptionList Text) -> DBFilterUI +filterUISchool schoolOptions = flip (prismAForm $ singletonFilter "school") $ aopt (lift `hoistField` selectField schoolOptions) (fslI MsgTableCourseSchool) + +filterUISheetSearch :: DBFilterUI +filterUISheetSearch mPrev = singletonMap "sheet-search" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgTableSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev))) + +filterUIIsRated :: DBFilterUI +filterUIIsRated = flip (prismAForm $ singletonFilter "israted" . maybePrism _PathPiece) $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableRatingTime) + +filterUISubmission :: DBFilterUI +filterUISubmission = flip (prismAForm $ singletonFilter "submission") $ aopt (lift `hoistField` textField) (fslI MsgTableSubmission) + +filterUIUserNameEmail :: DBFilterUI +filterUIUserNameEmail = flip (prismAForm $ singletonFilter "user-name-email") $ aopt textField (fslI MsgTableCourseMembers) + +filterUIUserMatrikelnummer :: DBFilterUI +filterUIUserMatrikelnummer = flip (prismAForm $ singletonFilter "user-matriclenumber") $ aopt textField (fslI MsgTableMatrikelNr) + +filterUICorrectorNameEmail :: DBFilterUI +filterUICorrectorNameEmail = flip (prismAForm $ singletonFilter "corrector-name-email") $ aopt textField (fslI MsgTableCorrector) + +filterUIIsAssigned :: DBFilterUI +filterUIIsAssigned = flip (prismAForm $ singletonFilter "isassigned" . maybePrism _PathPiece) $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableHasCorrector) + +filterUISubmissionGroup :: DBFilterUI +filterUISubmissionGroup = flip (prismAForm $ singletonFilter "submittors-group") $ aopt textField (fslI MsgTableSubmissionGroup) + +filterUIRating :: DBFilterUI +filterUIRating = flip (prismAForm $ singletonFilter "rating" . maybePrism _PathPiece) $ aopt (lift `hoistField` pointsField) (fslI MsgColumnRatingPoints) + +filterUIComment :: DBFilterUI +filterUIComment mPrev = singletonMap "comment" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgRatingComment) (Just <$> listToMaybe =<< (Map.lookup "comment" =<< mPrev)) + + makeCorrectionsTable :: ( IsDBTable m x, ToSortable h, Functor h ) => CorrectionTableWhere -> Colonnade h CorrectionTableData (DBCell m x) -> _ -> PSValidator m x -> DBParams m x -> DB (DBResult m x) makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtParams = do @@ -401,7 +442,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtParams , singletonMap "user-matriclenumber" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionUser `E.InnerJoin` user) -> do E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId E.where_ $ dbtRowKey row E.==. submissionUser E.^. SubmissionUserSubmission - E.where_ $ E.mkContainsFilter (E.^. UserMatrikelnummer) user (Set.singleton needle) + E.where_ $ E.mkContainsFilterWith Just (E.^. UserMatrikelnummer) user (Set.singleton needle) , singletonMap "submission-group" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser `E.InnerJoin` submissionUser) -> do E.on $ submissionUser E.^. SubmissionUserUser E.==. submissionGroupUser E.^. SubmissionGroupUserUser E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup @@ -663,13 +704,13 @@ postCorrectionsR = do , colRating , colRated ] -- Continue here - filterUI = Just $ \mPrev -> mconcat - [ prismAForm (singletonFilter "course" ) mPrev $ aopt (lift `hoistField` selectField courseOptions) (fslI MsgTableCourse) - , prismAForm (singletonFilter "term" ) mPrev $ aopt (lift `hoistField` selectField termOptions) (fslI MsgTableTerm) - , prismAForm (singletonFilter "school" ) mPrev $ aopt (lift `hoistField` selectField schoolOptions) (fslI MsgTableCourseSchool) - , Map.singleton "sheet-search" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgTableSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev))) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableRatingTime) - , prismAForm (singletonFilter "submission") mPrev $ aopt (lift `hoistField` textField) (fslI MsgTableSubmission) + filterUI = Just $ mconcat + [ filterUICourse courseOptions + , filterUITerm termOptions + , filterUISchool schoolOptions + , filterUISheetSearch + , filterUIIsRated + , filterUISubmission ] courseOptions = runDB $ do courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) @@ -709,16 +750,16 @@ postCCorrectionsR tid ssh csh = do , colCorrector , colAssigned ] -- Continue here - filterUI = Just $ \mPrev -> mconcat - [ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgTableCourseMembers) - , prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgTableMatrikelNr) + filterUI = Just $ mconcat + [ filterUIUserNameEmail + , filterUIUserMatrikelnummer -- "pseudonym" TODO DB only stores Word24 - , Map.singleton "sheet-search" . maybeToList <$> aopt textField (fslI MsgTableSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev))) - , prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgTableCorrector) - , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableHasCorrector) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableRatingTime) - , prismAForm (singletonFilter "submission-group") mPrev $ aopt textField (fslI MsgTableSubmissionGroup) - , prismAForm (singletonFilter "submission") mPrev $ aopt (lift `hoistField` textField) (fslI MsgTableSubmission) + , filterUISheetSearch + , filterUICorrectorNameEmail + , filterUIIsAssigned + , filterUIIsRated + , filterUISubmissionGroup + , filterUISubmission ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway correctionsR whereClause colonnade filterUI psValidator $ Map.fromList @@ -744,14 +785,14 @@ postSSubsR tid ssh csh shn = do , colCorrector , colAssigned ] - filterUI = Just $ \mPrev -> mconcat - [ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgTableCourseMembers) - , prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgTableMatrikelNr) - , prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgTableCorrector) - , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableHasCorrector) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableRatingTime) - , prismAForm (singletonFilter "submission-group") mPrev $ aopt textField (fslI MsgTableSubmissionGroup) - , prismAForm (singletonFilter "submission") mPrev $ aopt (lift `hoistField` textField) (fslI MsgTableSubmission) + filterUI = Just $ mconcat + [ filterUIUserNameEmail + , filterUIUserMatrikelnummer + , filterUICorrectorNameEmail + , filterUIIsAssigned + , filterUIIsRated + , filterUISubmissionGroup + , filterUISubmission -- "pseudonym" TODO DB only stores Word24 ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway diff --git a/src/Handler/Utils/Table/Pagination.hs b/src/Handler/Utils/Table/Pagination.hs index 18203825d..50e666ed0 100644 --- a/src/Handler/Utils/Table/Pagination.hs +++ b/src/Handler/Utils/Table/Pagination.hs @@ -1171,7 +1171,6 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db && all (is _Just) filterSql psLimit' = bool PagesizeAll psLimit selectPagesize - rows' <- E.select . E.from $ \t -> do res <- dbtSQLQuery t @@ -1184,10 +1183,10 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db Nothing | PagesizeLimit l <- psLimit' , selectPagesize + , hasn't (_FormSuccess . _DBCsvExport) csvMode -> do - unless (has (_FormSuccess . _DBCsvExport) csvMode) $ - E.limit l - E.offset (psPage * l) + E.limit l + E.offset $ psPage * l Just ps -> E.where_ $ dbtRowKey t `E.sqlIn` ps _other -> return () Map.foldr (\fc expr -> maybe (return ()) (E.where_ . ($ t)) fc >> expr) (return ()) filterSql From 153af8c6b4042430bb4bc120fa5c24a5d114e4c1 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Tue, 17 Aug 2021 12:30:08 +0200 Subject: [PATCH 013/143] feat(corrections-r): filter/sort by pseudonym --- .../courses/submission/de-de-formal.msg | 1 + .../categories/courses/submission/en-eu.msg | 1 + src/Handler/Course/User.hs | 1 + src/Handler/Submission/Grade.hs | 1 + src/Handler/Submission/List.hs | 24 +++++++++++++++---- 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/messages/uniworx/categories/courses/submission/de-de-formal.msg b/messages/uniworx/categories/courses/submission/de-de-formal.msg index 165cfe9a9..54a7795d5 100644 --- a/messages/uniworx/categories/courses/submission/de-de-formal.msg +++ b/messages/uniworx/categories/courses/submission/de-de-formal.msg @@ -68,6 +68,7 @@ Corrected: Korrigiert HeadingSubmissionEditHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{tid}-#{ssh}-#{csh} #{sheetName}: Abgabe editieren/anlegen SubmissionUsers: Studenten AssignedTime: Zuteilung +SubmissionPseudonym !ident-ok: Pseudonym Pseudonyms: Pseudonyme CourseCorrectionsTitle: Korrekturen für diesen Kurs SubmissionArchiveName: abgaben diff --git a/messages/uniworx/categories/courses/submission/en-eu.msg b/messages/uniworx/categories/courses/submission/en-eu.msg index f9efeb3a0..e7f96147c 100644 --- a/messages/uniworx/categories/courses/submission/en-eu.msg +++ b/messages/uniworx/categories/courses/submission/en-eu.msg @@ -66,6 +66,7 @@ Corrected: Marked HeadingSubmissionEditHead tid ssh csh sheetName: #{tid}-#{ssh}-#{csh} #{sheetName}: Edit/Create submission SubmissionUsers: Submittors AssignedTime: Assigned +SubmissionPseudonym !ident-ok: Pseudonym Pseudonyms: Pseudonyms CourseCorrectionsTitle: Corrections for this course SubmissionArchiveName: submissions diff --git a/src/Handler/Course/User.hs b/src/Handler/Course/User.hs index 4accc76cf..5db9da78b 100644 --- a/src/Handler/Course/User.hs +++ b/src/Handler/Course/User.hs @@ -260,6 +260,7 @@ courseUserSubmissionsSection (Entity cid Course{..}) (Entity uid _) = do filterUI = Just $ mconcat [ filterUIUserNameEmail , filterUIUserMatrikelnummer + , filterUIPseudonym , filterUISheetSearch , filterUICorrectorNameEmail , filterUIIsAssigned diff --git a/src/Handler/Submission/Grade.hs b/src/Handler/Submission/Grade.hs index 41a869ff2..e848d2901 100644 --- a/src/Handler/Submission/Grade.hs +++ b/src/Handler/Submission/Grade.hs @@ -43,6 +43,7 @@ postCorrectionsGradeR = do , filterUITerm termOptions , filterUISchool schoolOptions , filterUISheetSearch + , filterUIPseudonym , filterUIIsRated -- , flip (prismAForm $ singletonFilter "rating-visible" . maybePrism _PathPiece) $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingDone) , filterUIRating diff --git a/src/Handler/Submission/List.hs b/src/Handler/Submission/List.hs index 99b47cdda..72bce9202 100644 --- a/src/Handler/Submission/List.hs +++ b/src/Handler/Submission/List.hs @@ -9,7 +9,7 @@ module Handler.Submission.List , restrictAnonymous, restrictCorrector , ratedBy, courseIs, sheetIs, userIs , colTerm, colSchool, colCourse, colSheet, colCorrector, colSubmissionLink, colSelect, colSubmittors, colSMatrikel, colRating, colAssigned, colRated, colPseudonyms, colRatedField, colPointsField, colMaxPointsField, colCommentField, colLastEdit, colSGroups - , filterUICourse, filterUITerm, filterUISchool, filterUISheetSearch, filterUIIsRated, filterUISubmission, filterUIUserNameEmail, filterUIUserMatrikelnummer, filterUICorrectorNameEmail, filterUIIsAssigned, filterUISubmissionGroup, filterUIRating, filterUIComment + , filterUICourse, filterUITerm, filterUISchool, filterUISheetSearch, filterUIIsRated, filterUISubmission, filterUIUserNameEmail, filterUIUserMatrikelnummer, filterUICorrectorNameEmail, filterUIIsAssigned, filterUISubmissionGroup, filterUIRating, filterUIComment, filterUIPseudonym , makeCorrectionsTable , CorrectionTableData, CorrectionTableWhere , ActionCorrections(..), downloadAction, deleteAction, assignAction, autoAssignAction @@ -40,13 +40,15 @@ import Database.Persist.Sql (updateWhereCount) import Data.List (genericLength) -newtype CorrectionTableFilterProj = CorrectionTableFilterProj +data CorrectionTableFilterProj = CorrectionTableFilterProj { corrProjFilterSubmission :: Maybe (Set [CI Char]) + , corrProjFilterPseudonym :: Maybe (Set [CI Char]) } instance Default CorrectionTableFilterProj where def = CorrectionTableFilterProj { corrProjFilterSubmission = Nothing + , corrProjFilterPseudonym = Nothing } makeLenses_ ''CorrectionTableFilterProj @@ -307,6 +309,9 @@ filterUIIsRated = flip (prismAForm $ singletonFilter "israted" . maybePrism _Pat filterUISubmission :: DBFilterUI filterUISubmission = flip (prismAForm $ singletonFilter "submission") $ aopt (lift `hoistField` textField) (fslI MsgTableSubmission) + +filterUIPseudonym :: DBFilterUI +filterUIPseudonym = flip (prismAForm $ singletonFilter "pseudonym") $ aopt (lift `hoistField` textField) (fslI MsgSubmissionPseudonym) filterUIUserNameEmail :: DBFilterUI filterUIUserNameEmail = flip (prismAForm $ singletonFilter "user-name-email") $ aopt textField (fslI MsgTableCourseMembers) @@ -357,6 +362,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtParams return (submission, sheet, crse, corrector, lastEdit) dbtProj = (views _dbtProjRow . set _dbrOutput) =<< do (submission@(Entity sId _), sheet@(Entity shId Sheet{..}), (E.Value courseName, E.Value courseShorthand, E.Value courseTerm, E.Value courseSchool), mCorrector, E.Value mbLastEdit) <- view $ _dbtProjRow . _dbrOutput + cid <- encrypt sId forMM_ (view $ _dbtProjFilter . _corrProjFilterSubmission) $ \criteria -> let haystack = map CI.mk . unpack $ toPathPiece cid @@ -377,6 +383,11 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtParams return (user, pseudonym E.?. SheetPseudonymPseudonym, submissionGroup') let submittorMap = List.foldr (\(Entity userId user, E.Value pseudo, E.Value sGroup) -> Map.insert userId (user, pseudo, sGroup)) Map.empty submittors + + forMM_ (view $ _dbtProjFilter . _corrProjFilterPseudonym) $ \criteria -> + let haystacks = setOf (folded . resultUserPseudonym . re _PseudonymText . to (map CI.mk . unpack)) submittorMap + in guard $ any (\haystack -> any (`isInfixOf` haystack) criteria) haystacks + nonAnonymousAccess <- lift . lift $ or2M (return $ not sheetAnonymousCorrection) (hasReadAccessTo $ CourseR courseTerm courseSchool courseShorthand CCorrectionsR) @@ -409,6 +420,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtParams , singletonMap "submittors" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserUser . $(multifocusG 2) _userSurname _userDisplayName) x , singletonMap "submittors-matriculation" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserUser . _userMatrikelnummer . _Just) x , singletonMap "submittors-group" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserSubmissionGroup) x + , singletonMap "submittors-pseudonyms" . SortProjected . comparing $ \x -> setOf (resultSubmittors . resultUserPseudonym . re _PseudonymText) x , singletonMap "comment" . SortColumn $ views querySubmission (E.^. SubmissionRatingComment) -- sorting by comment specifically requested by correctors to easily see submissions to be done , singletonMap "last-edit" . SortColumn $ view queryLastEdit , singletonMap "submission" . SortProjected . comparing $ toPathPiece . view resultCryptoID @@ -453,6 +465,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtParams , singletonMap "rating" . FilterColumn . E.mkExactFilterWith Just $ views querySubmission (E.^. SubmissionRatingPoints) , singletonMap "comment" . FilterColumn . E.mkContainsFilterWith Just $ views querySubmission (E.^. SubmissionRatingComment) , singletonMap "submission" $ FilterProjected (_corrProjFilterSubmission ?~) + , singletonMap "pseudonym" $ FilterProjected (_corrProjFilterPseudonym ?~) ] , dbtFilterUI = fromMaybe mempty dbtFilterUI , dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (const defaultDBSFilterLayout) dbtFilterUI } @@ -705,7 +718,8 @@ postCorrectionsR = do , colRated ] -- Continue here filterUI = Just $ mconcat - [ filterUICourse courseOptions + [ filterUIPseudonym + , filterUICourse courseOptions , filterUITerm termOptions , filterUISchool schoolOptions , filterUISheetSearch @@ -753,7 +767,7 @@ postCCorrectionsR tid ssh csh = do filterUI = Just $ mconcat [ filterUIUserNameEmail , filterUIUserMatrikelnummer - -- "pseudonym" TODO DB only stores Word24 + , filterUIPseudonym , filterUISheetSearch , filterUICorrectorNameEmail , filterUIIsAssigned @@ -788,12 +802,12 @@ postSSubsR tid ssh csh shn = do filterUI = Just $ mconcat [ filterUIUserNameEmail , filterUIUserMatrikelnummer + , filterUIPseudonym , filterUICorrectorNameEmail , filterUIIsAssigned , filterUIIsRated , filterUISubmissionGroup , filterUISubmission - -- "pseudonym" TODO DB only stores Word24 ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway correctionsR whereClause colonnade filterUI psValidator $ Map.fromList From 57ea5fe329e3013bff83fffb2f8ad999cf9f5b6f Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Tue, 17 Aug 2021 12:46:27 +0200 Subject: [PATCH 014/143] refactor(corrections-r): modernize --- src/Handler/Submission/List.hs | 244 ++++++++++++++++----------------- 1 file changed, 119 insertions(+), 125 deletions(-) diff --git a/src/Handler/Submission/List.hs b/src/Handler/Submission/List.hs index 72bce9202..a9959fdd1 100644 --- a/src/Handler/Submission/List.hs +++ b/src/Handler/Submission/List.hs @@ -337,144 +337,138 @@ filterUIComment mPrev = singletonMap "comment" . maybeToList <$> aopt (lift `hoi makeCorrectionsTable :: ( IsDBTable m x, ToSortable h, Functor h ) => CorrectionTableWhere -> Colonnade h CorrectionTableData (DBCell m x) -> _ -> PSValidator m x -> DBParams m x -> DB (DBResult m x) -makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtParams = do - let dbtSQLQuery = runReaderT $ do - course <- view queryCourse - sheet <- view querySheet - submission <- view querySubmission - corrector <- view queryCorrector +makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' psValidator dbtParams + = let dbtSQLQuery = runReaderT $ do + course <- view queryCourse + sheet <- view querySheet + submission <- view querySubmission + corrector <- view queryCorrector - lift $ do - E.on $ corrector E.?. UserId E.==. submission E.^. SubmissionRatingBy - E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet - E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse + lift $ do + E.on $ corrector E.?. UserId E.==. submission E.^. SubmissionRatingBy + E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet + E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse - lastEdit <- view queryLastEdit + lastEdit <- view queryLastEdit - let crse = ( course E.^. CourseName - , course E.^. CourseShorthand - , course E.^. CourseTerm - , course E.^. CourseSchool - ) + let crse = ( course E.^. CourseName + , course E.^. CourseShorthand + , course E.^. CourseTerm + , course E.^. CourseSchool + ) - lift . E.where_ =<< whereClause + lift . E.where_ =<< whereClause - return (submission, sheet, crse, corrector, lastEdit) - dbtProj = (views _dbtProjRow . set _dbrOutput) =<< do - (submission@(Entity sId _), sheet@(Entity shId Sheet{..}), (E.Value courseName, E.Value courseShorthand, E.Value courseTerm, E.Value courseSchool), mCorrector, E.Value mbLastEdit) <- view $ _dbtProjRow . _dbrOutput + return (submission, sheet, crse, corrector, lastEdit) + dbtProj = (views _dbtProjRow . set _dbrOutput) =<< do + (submission@(Entity sId _), sheet@(Entity shId Sheet{..}), (E.Value courseName, E.Value courseShorthand, E.Value courseTerm, E.Value courseSchool), mCorrector, E.Value mbLastEdit) <- view $ _dbtProjRow . _dbrOutput - cid <- encrypt sId - forMM_ (view $ _dbtProjFilter . _corrProjFilterSubmission) $ \criteria -> - let haystack = map CI.mk . unpack $ toPathPiece cid - in guard $ any (`isInfixOf` haystack) criteria + cid <- encrypt sId + forMM_ (view $ _dbtProjFilter . _corrProjFilterSubmission) $ \criteria -> + let haystack = map CI.mk . unpack $ toPathPiece cid + in guard $ any (`isInfixOf` haystack) criteria - submittors <- lift . lift . E.select . E.from $ \((submissionUser `E.InnerJoin` user) `E.LeftOuterJoin` pseudonym) -> do - E.on $ pseudonym E.?. SheetPseudonymUser E.==. E.just (user E.^. UserId) - E.&&. pseudonym E.?. SheetPseudonymSheet E.==. E.just (E.val shId) - E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId - E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val sId - E.orderBy [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName] - let submissionGroup' = E.subSelectMaybe . E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser) -> do - E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup - E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val sheetCourse - E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. user E.^. UserId - return . E.just $ submissionGroup E.^. SubmissionGroupName + submittors <- lift . lift . E.select . E.from $ \((submissionUser `E.InnerJoin` user) `E.LeftOuterJoin` pseudonym) -> do + E.on $ pseudonym E.?. SheetPseudonymUser E.==. E.just (user E.^. UserId) + E.&&. pseudonym E.?. SheetPseudonymSheet E.==. E.just (E.val shId) + E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId + E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val sId + E.orderBy [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName] + let submissionGroup' = E.subSelectMaybe . E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser) -> do + E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup + E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val sheetCourse + E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. user E.^. UserId + return . E.just $ submissionGroup E.^. SubmissionGroupName - return (user, pseudonym E.?. SheetPseudonymPseudonym, submissionGroup') - let - submittorMap = List.foldr (\(Entity userId user, E.Value pseudo, E.Value sGroup) -> Map.insert userId (user, pseudo, sGroup)) Map.empty submittors + return (user, pseudonym E.?. SheetPseudonymPseudonym, submissionGroup') + let + submittorMap = List.foldr (\(Entity userId user, E.Value pseudo, E.Value sGroup) -> Map.insert userId (user, pseudo, sGroup)) Map.empty submittors - forMM_ (view $ _dbtProjFilter . _corrProjFilterPseudonym) $ \criteria -> - let haystacks = setOf (folded . resultUserPseudonym . re _PseudonymText . to (map CI.mk . unpack)) submittorMap - in guard $ any (\haystack -> any (`isInfixOf` haystack) criteria) haystacks + forMM_ (view $ _dbtProjFilter . _corrProjFilterPseudonym) $ \criteria -> + let haystacks = setOf (folded . resultUserPseudonym . re _PseudonymText . to (map CI.mk . unpack)) submittorMap + in guard $ any (\haystack -> any (`isInfixOf` haystack) criteria) haystacks - nonAnonymousAccess <- lift . lift $ or2M - (return $ not sheetAnonymousCorrection) - (hasReadAccessTo $ CourseR courseTerm courseSchool courseShorthand CCorrectionsR) + nonAnonymousAccess <- lift . lift $ or2M + (return $ not sheetAnonymousCorrection) + (hasReadAccessTo $ CourseR courseTerm courseSchool courseShorthand CCorrectionsR) - return (submission, sheet, (courseName, courseShorthand, courseTerm, courseSchool), mCorrector, mbLastEdit, submittorMap, cid, nonAnonymousAccess) - dbtRowKey = views querySubmission (E.^. SubmissionId) - dbTable psValidator DBTable - { dbtSQLQuery - , dbtRowKey - , dbtColonnade - , dbtProj - , dbtSorting = mconcat - [ singletonMap "term" . SortColumn $ views queryCourse (E.^. CourseTerm) - , singletonMap "school" . SortColumn $ views queryCourse (E.^. CourseSchool) - , singletonMap "course" . SortColumn $ views queryCourse (E.^. CourseShorthand) - , singletonMap "sheet" . SortColumn $ views querySheet (E.^. SheetName) - , singletonMap "corrector" . SortColumns $ \x -> - [ SomeExprValue (views queryCorrector (E.?. UserSurname) x) - , SomeExprValue (views queryCorrector (E.?. UserDisplayName) x) + return (submission, sheet, (courseName, courseShorthand, courseTerm, courseSchool), mCorrector, mbLastEdit, submittorMap, cid, nonAnonymousAccess) + dbtRowKey = views querySubmission (E.^. SubmissionId) + dbtSorting = mconcat + [ singletonMap "term" . SortColumn $ views queryCourse (E.^. CourseTerm) + , singletonMap "school" . SortColumn $ views queryCourse (E.^. CourseSchool) + , singletonMap "course" . SortColumn $ views queryCourse (E.^. CourseShorthand) + , singletonMap "sheet" . SortColumn $ views querySheet (E.^. SheetName) + , singletonMap "corrector" . SortColumns $ \x -> + [ SomeExprValue (views queryCorrector (E.?. UserSurname) x) + , SomeExprValue (views queryCorrector (E.?. UserDisplayName) x) + ] + , singletonMap "rating" . SortColumn $ views querySubmission (E.^. SubmissionRatingPoints) + , singletonMap "sheet-type" . SortColumns $ \(view querySheet -> sheet) -> + [ SomeExprValue ((sheet E.^. SheetType) E.->. "type" :: E.SqlExpr (E.Value Value)) + , SomeExprValue (((sheet E.^. SheetType) E.->. "grading" :: E.SqlExpr (E.Value Value)) E.->. "max" :: E.SqlExpr (E.Value Value)) + , SomeExprValue (((sheet E.^. SheetType) E.->. "grading" :: E.SqlExpr (E.Value Value)) E.->. "passing" :: E.SqlExpr (E.Value Value)) + ] + , singletonMap "israted" . SortColumn $ views querySubmission $ E.not_ . E.isNothing . (E.^. SubmissionRatingTime) + , singletonMap "ratingtime" . SortColumn $ views querySubmission (E.^. SubmissionRatingTime) + , singletonMap "assignedtime" . SortColumn $ views querySubmission (E.^. SubmissionRatingAssigned) + , singletonMap "submittors" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserUser . $(multifocusG 2) _userSurname _userDisplayName) x + , singletonMap "submittors-matriculation" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserUser . _userMatrikelnummer . _Just) x + , singletonMap "submittors-group" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserSubmissionGroup) x + , singletonMap "submittors-pseudonyms" . SortProjected . comparing $ \x -> setOf (resultSubmittors . resultUserPseudonym . re _PseudonymText) x + , singletonMap "comment" . SortColumn $ views querySubmission (E.^. SubmissionRatingComment) -- sorting by comment specifically requested by correctors to easily see submissions to be done + , singletonMap "last-edit" . SortColumn $ view queryLastEdit + , singletonMap "submission" . SortProjected . comparing $ toPathPiece . view resultCryptoID ] - , singletonMap "rating" . SortColumn $ views querySubmission (E.^. SubmissionRatingPoints) - , singletonMap "sheet-type" . SortColumns $ \(view querySheet -> sheet) -> - [ SomeExprValue ((sheet E.^. SheetType) E.->. "type" :: E.SqlExpr (E.Value Value)) - , SomeExprValue (((sheet E.^. SheetType) E.->. "grading" :: E.SqlExpr (E.Value Value)) E.->. "max" :: E.SqlExpr (E.Value Value)) - , SomeExprValue (((sheet E.^. SheetType) E.->. "grading" :: E.SqlExpr (E.Value Value)) E.->. "passing" :: E.SqlExpr (E.Value Value)) + dbtFilter = mconcat + [ singletonMap "term" . FilterColumn . E.mkExactFilter $ views queryCourse (E.^. CourseTerm) + , singletonMap "school" . FilterColumn . E.mkExactFilter $ views queryCourse (E.^. CourseSchool) + , singletonMap "course" . FilterColumn . E.mkExactFilter $ views queryCourse (E.^. CourseShorthand) + , singletonMap "sheet" . FilterColumn . E.mkExactFilter $ views querySheet (E.^. SheetName) + , singletonMap "sheet-search" . FilterColumn . E.mkContainsFilter $ views querySheet (E.^. SheetName) + , singletonMap "corrector" . FilterColumn . E.mkExactFilterWith Just $ views queryCorrector (E.?. UserIdent) + , singletonMap "isassigned" . FilterColumn . E.mkExactFilterLast $ views querySubmission (E.isJust . (E.^. SubmissionRatingBy)) + , singletonMap "israted" . FilterColumn . E.mkExactFilterLast $ views querySubmission sqlSubmissionRatingDone + , singletonMap "corrector-name-email" . FilterColumn $ E.anyFilter + [ E.mkContainsFilterWith Just $ views queryCorrector (E.?. UserSurname) + , E.mkContainsFilterWith Just $ views queryCorrector (E.?. UserDisplayName) + , E.mkContainsFilterWith (Just . CI.mk) $ views queryCorrector (E.?. UserEmail) + , E.mkContainsFilterWith (Just . CI.mk) $ views queryCorrector (E.?. UserIdent) + , E.mkContainsFilterWith (Just . CI.mk) $ views queryCorrector (E.?. UserDisplayEmail) + ] + , singletonMap "user-name-email" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionUser `E.InnerJoin` user) -> do + E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId + E.where_ $ dbtRowKey row E.==. submissionUser E.^. SubmissionUserSubmission + E.where_ $ E.anyFilter + [ E.mkContainsFilter (E.^. UserSurname) + , E.mkContainsFilter (E.^. UserDisplayName) + , E.mkContainsFilterWith CI.mk (E.^. UserEmail) + , E.mkContainsFilterWith CI.mk (E.^. UserIdent) + , E.mkContainsFilterWith CI.mk (E.^. UserDisplayEmail) + ] user (Set.singleton needle) + , singletonMap "user-matriclenumber" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionUser `E.InnerJoin` user) -> do + E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId + E.where_ $ dbtRowKey row E.==. submissionUser E.^. SubmissionUserSubmission + E.where_ $ E.mkContainsFilterWith Just (E.^. UserMatrikelnummer) user (Set.singleton needle) + , singletonMap "submission-group" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser `E.InnerJoin` submissionUser) -> do + E.on $ submissionUser E.^. SubmissionUserUser E.==. submissionGroupUser E.^. SubmissionGroupUserUser + E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup + E.where_ $ (row ^. queryCourse) E.^. CourseId E.==. submissionGroup E.^. SubmissionGroupCourse + E.&&. dbtRowKey row E.==. submissionUser E.^. SubmissionUserSubmission + E.where_ $ E.mkContainsFilter (E.^. SubmissionGroupName) submissionGroup (Set.singleton needle) + , singletonMap "rating-visible" . FilterColumn . E.mkExactFilterLast $ views querySubmission sqlSubmissionRatingDone -- TODO: Identical with israted? + , singletonMap "rating" . FilterColumn . E.mkExactFilterWith Just $ views querySubmission (E.^. SubmissionRatingPoints) + , singletonMap "comment" . FilterColumn . E.mkContainsFilterWith Just $ views querySubmission (E.^. SubmissionRatingComment) + , singletonMap "submission" $ FilterProjected (_corrProjFilterSubmission ?~) + , singletonMap "pseudonym" $ FilterProjected (_corrProjFilterPseudonym ?~) ] - , singletonMap "israted" . SortColumn $ views querySubmission $ E.not_ . E.isNothing . (E.^. SubmissionRatingTime) - , singletonMap "ratingtime" . SortColumn $ views querySubmission (E.^. SubmissionRatingTime) - , singletonMap "assignedtime" . SortColumn $ views querySubmission (E.^. SubmissionRatingAssigned) - , singletonMap "submittors" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserUser . $(multifocusG 2) _userSurname _userDisplayName) x - , singletonMap "submittors-matriculation" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserUser . _userMatrikelnummer . _Just) x - , singletonMap "submittors-group" . SortProjected . comparing $ \x -> guardOn @Maybe (x ^. resultNonAnonymousAccess) $ setOf (resultSubmittors . resultUserSubmissionGroup) x - , singletonMap "submittors-pseudonyms" . SortProjected . comparing $ \x -> setOf (resultSubmittors . resultUserPseudonym . re _PseudonymText) x - , singletonMap "comment" . SortColumn $ views querySubmission (E.^. SubmissionRatingComment) -- sorting by comment specifically requested by correctors to easily see submissions to be done - , singletonMap "last-edit" . SortColumn $ view queryLastEdit - , singletonMap "submission" . SortProjected . comparing $ toPathPiece . view resultCryptoID - ] - , dbtFilter = mconcat - [ singletonMap "term" . FilterColumn . E.mkExactFilter $ views queryCourse (E.^. CourseTerm) - , singletonMap "school" . FilterColumn . E.mkExactFilter $ views queryCourse (E.^. CourseSchool) - , singletonMap "course" . FilterColumn . E.mkExactFilter $ views queryCourse (E.^. CourseShorthand) - , singletonMap "sheet" . FilterColumn . E.mkExactFilter $ views querySheet (E.^. SheetName) - , singletonMap "sheet-search" . FilterColumn . E.mkContainsFilter $ views querySheet (E.^. SheetName) - , singletonMap "corrector" . FilterColumn . E.mkExactFilterWith Just $ views queryCorrector (E.?. UserIdent) - , singletonMap "isassigned" . FilterColumn . E.mkExactFilterLast $ views querySubmission (E.isJust . (E.^. SubmissionRatingBy)) - , singletonMap "israted" . FilterColumn . E.mkExactFilterLast $ views querySubmission sqlSubmissionRatingDone - , singletonMap "corrector-name-email" . FilterColumn $ E.anyFilter - [ E.mkContainsFilterWith Just $ views queryCorrector (E.?. UserSurname) - , E.mkContainsFilterWith Just $ views queryCorrector (E.?. UserDisplayName) - , E.mkContainsFilterWith (Just . CI.mk) $ views queryCorrector (E.?. UserEmail) - , E.mkContainsFilterWith (Just . CI.mk) $ views queryCorrector (E.?. UserIdent) - , E.mkContainsFilterWith (Just . CI.mk) $ views queryCorrector (E.?. UserDisplayEmail) - ] - , singletonMap "user-name-email" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionUser `E.InnerJoin` user) -> do - E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId - E.where_ $ dbtRowKey row E.==. submissionUser E.^. SubmissionUserSubmission - E.where_ $ E.anyFilter - [ E.mkContainsFilter (E.^. UserSurname) - , E.mkContainsFilter (E.^. UserDisplayName) - , E.mkContainsFilterWith CI.mk (E.^. UserEmail) - , E.mkContainsFilterWith CI.mk (E.^. UserIdent) - , E.mkContainsFilterWith CI.mk (E.^. UserDisplayEmail) - ] user (Set.singleton needle) - , singletonMap "user-matriclenumber" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionUser `E.InnerJoin` user) -> do - E.on $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId - E.where_ $ dbtRowKey row E.==. submissionUser E.^. SubmissionUserSubmission - E.where_ $ E.mkContainsFilterWith Just (E.^. UserMatrikelnummer) user (Set.singleton needle) - , singletonMap "submission-group" . FilterColumn $ E.mkExistsFilter $ \row needle -> E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser `E.InnerJoin` submissionUser) -> do - E.on $ submissionUser E.^. SubmissionUserUser E.==. submissionGroupUser E.^. SubmissionGroupUserUser - E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup - E.where_ $ (row ^. queryCourse) E.^. CourseId E.==. submissionGroup E.^. SubmissionGroupCourse - E.&&. dbtRowKey row E.==. submissionUser E.^. SubmissionUserSubmission - E.where_ $ E.mkContainsFilter (E.^. SubmissionGroupName) submissionGroup (Set.singleton needle) - , singletonMap "rating-visible" . FilterColumn . E.mkExactFilterLast $ views querySubmission sqlSubmissionRatingDone -- TODO: Identical with israted? - , singletonMap "rating" . FilterColumn . E.mkExactFilterWith Just $ views querySubmission (E.^. SubmissionRatingPoints) - , singletonMap "comment" . FilterColumn . E.mkContainsFilterWith Just $ views querySubmission (E.^. SubmissionRatingComment) - , singletonMap "submission" $ FilterProjected (_corrProjFilterSubmission ?~) - , singletonMap "pseudonym" $ FilterProjected (_corrProjFilterPseudonym ?~) - ] - , dbtFilterUI = fromMaybe mempty dbtFilterUI - , dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (const defaultDBSFilterLayout) dbtFilterUI } - , dbtParams - , dbtIdent = "corrections" :: Text - , dbtCsvEncode = noCsvEncode - , dbtCsvDecode = Nothing - , dbtExtraReps = [] - } + dbtFilterUI = fromMaybe mempty dbtFilterUI' + dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (const defaultDBSFilterLayout) dbtFilterUI' } + dbtIdent = "corrections" :: Text + dbtCsvEncode = noCsvEncode + dbtCsvDecode = Nothing + dbtExtraReps = [] + in dbTable psValidator DBTable{..} data ActionCorrections = CorrDownload | CorrSetCorrector From 51522efc7c9915115e0d8791320a03e35d2933c8 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Tue, 17 Aug 2021 14:38:52 +0200 Subject: [PATCH 015/143] feat(corrections-r): authorship statement state --- src/Handler/Submission/Grade.hs | 2 +- src/Handler/Submission/Helper.hs | 32 +-------- src/Handler/Submission/List.hs | 120 ++++++++++++++++++++++--------- src/Handler/Utils/Submission.hs | 58 +++++++++++++++ 4 files changed, 147 insertions(+), 65 deletions(-) diff --git a/src/Handler/Submission/Grade.hs b/src/Handler/Submission/Grade.hs index e848d2901..1ddb8019e 100644 --- a/src/Handler/Submission/Grade.hs +++ b/src/Handler/Submission/Grade.hs @@ -62,7 +62,7 @@ postCorrectionsGradeR = do & restrictAnonymous & restrictCorrector & defaultSorting [SortDescBy "ratingtime"] :: PSValidator (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult SubmissionId (Bool, Maybe Points, Maybe Text) CorrectionTableData)) - unFormResult = getDBFormResult $ \DBRow{ dbrOutput = (Entity _ sub@Submission{..}, _, _, _, _, _, _, _) } -> (submissionRatingDone sub, submissionRatingPoints, submissionRatingComment) + unFormResult = getDBFormResult $ \(view $ resultSubmission . _entityVal -> sub@Submission{..}) -> (submissionRatingDone sub, submissionRatingPoints, submissionRatingComment) (fmap unFormResult -> tableRes, table) <- runDB $ makeCorrectionsTable whereClause displayColumns filterUI psValidator $ def { dbParamsFormAction = Just $ SomeRoute CorrectionsGradeR diff --git a/src/Handler/Submission/Helper.hs b/src/Handler/Submission/Helper.hs index c78335edf..3b6521f1b 100644 --- a/src/Handler/Submission/Helper.hs +++ b/src/Handler/Submission/Helper.hs @@ -31,18 +31,6 @@ import Handler.Submission.SubmissionUserInvite import qualified Data.Conduit.Combinators as C -data AuthorshipStatementSubmissionState - = ASExists - | ASOldStatement - | ASMissing - deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) - deriving anyclass (Universe, Finite) - -nullaryPathPiece ''AuthorshipStatementSubmissionState $ camelToPathPiece' 1 - -embedRenderMessage ''UniWorX ''AuthorshipStatementSubmissionState $ concat . ("SubmissionAuthorshipStatementState" :) . drop 1 . splitCamel - - makeSubmissionForm :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX, MonadThrow m) => CourseId -> SheetId -> Maybe (Entity AuthorshipStatementDefinition) -> Maybe SubmissionId -> UploadMode -> SheetGroup -> Maybe FileUploads -> Bool -> Set (Either UserEmail UserId) -> (Markup -> MForm (ReaderT SqlBackend m) (FormResult (Maybe FileUploads, Set (Either UserEmail UserId), Maybe AuthorshipStatementDefinitionId), Widget)) @@ -606,28 +594,10 @@ submissionHelper tid ssh csh shn mcid = do subUsers <- maybeT (return []) $ do subId <- hoistMaybe msmid - let - getUserAuthorshipStatement :: UserId - -> DB AuthorshipStatementSubmissionState - getUserAuthorshipStatement uid = runConduit $ - getStmts - .| fmap toRes (execWriterC . C.mapM_ $ tell . toPoint) - where - getStmts = E.selectSource . E.from $ \authorshipStatementSubmission -> do - E.where_ $ authorshipStatementSubmission E.^. AuthorshipStatementSubmissionSubmission E.==. E.val subId - E.&&. authorshipStatementSubmission E.^. AuthorshipStatementSubmissionUser E.==. E.val uid - return authorshipStatementSubmission - toPoint :: Entity AuthorshipStatementSubmission -> Maybe Any - toPoint (Entity _ AuthorshipStatementSubmission{..}) = Just . Any $ fmap entityKey mASDefinition == Just authorshipStatementSubmissionStatement - toRes :: Maybe Any -> AuthorshipStatementSubmissionState - toRes = \case - Just (Any True) -> ASExists - Just (Any False) -> ASOldStatement - Nothing -> ASMissing lift $ buddies & bool id (maybe id (Set.insert . Right) muid) isOwner & Set.toList - & mapMOf (traverse . _Right) (\uid -> (,,) <$> (encrypt uid :: DB CryptoUUIDUser) <*> getJust uid <*> getUserAuthorshipStatement uid) + & mapMOf (traverse . _Right) (\uid -> (,,) <$> (encrypt uid :: DB CryptoUUIDUser) <*> getJust uid <*> getUserAuthorshipStatement mASDefinition subId uid) & fmap (sortOn . over _Right $ (,,,) <$> views _2 userSurname <*> views _2 userDisplayName <*> views _2 userEmail <*> view _1) subUsersVisible <- orM diff --git a/src/Handler/Submission/List.hs b/src/Handler/Submission/List.hs index a9959fdd1..14f1fdb29 100644 --- a/src/Handler/Submission/List.hs +++ b/src/Handler/Submission/List.hs @@ -8,8 +8,9 @@ module Handler.Submission.List , correctionsR' , restrictAnonymous, restrictCorrector , ratedBy, courseIs, sheetIs, userIs - , colTerm, colSchool, colCourse, colSheet, colCorrector, colSubmissionLink, colSelect, colSubmittors, colSMatrikel, colRating, colAssigned, colRated, colPseudonyms, colRatedField, colPointsField, colMaxPointsField, colCommentField, colLastEdit, colSGroups - , filterUICourse, filterUITerm, filterUISchool, filterUISheetSearch, filterUIIsRated, filterUISubmission, filterUIUserNameEmail, filterUIUserMatrikelnummer, filterUICorrectorNameEmail, filterUIIsAssigned, filterUISubmissionGroup, filterUIRating, filterUIComment, filterUIPseudonym + , resultSubmission + , colTerm, colSchool, colCourse, colSheet, colCorrector, colSubmissionLink, colSelect, colSubmittors, colSMatrikel, colRating, colAssigned, colRated, colPseudonyms, colRatedField, colPointsField, colMaxPointsField, colCommentField, colLastEdit, colSGroups, colAuthorshipStatementState + , filterUICourse, filterUITerm, filterUISchool, filterUISheetSearch, filterUIIsRated, filterUISubmission, filterUIUserNameEmail, filterUIUserMatrikelnummer, filterUICorrectorNameEmail, filterUIIsAssigned, filterUISubmissionGroup, filterUIRating, filterUIComment, filterUIPseudonym, filterUIAuthorshipStatementState , makeCorrectionsTable , CorrectionTableData, CorrectionTableWhere , ActionCorrections(..), downloadAction, deleteAction, assignAction, autoAssignAction @@ -33,6 +34,8 @@ import Database.Esqueleto.Utils.TH import qualified Database.Esqueleto.Legacy as E import qualified Database.Esqueleto.Utils as E +import qualified Data.Conduit.Combinators as C + import Text.Hamlet (ihamletFile) import Database.Persist.Sql (updateWhereCount) @@ -43,12 +46,14 @@ import Data.List (genericLength) data CorrectionTableFilterProj = CorrectionTableFilterProj { corrProjFilterSubmission :: Maybe (Set [CI Char]) , corrProjFilterPseudonym :: Maybe (Set [CI Char]) + , corrProjFilterAuthorshipStatementState :: Last AuthorshipStatementSubmissionState } instance Default CorrectionTableFilterProj where def = CorrectionTableFilterProj { corrProjFilterSubmission = Nothing , corrProjFilterPseudonym = Nothing + , corrProjFilterAuthorshipStatementState = Last Nothing } makeLenses_ ''CorrectionTableFilterProj @@ -70,6 +75,7 @@ type CorrectionTableData = DBRow ( Entity Submission , Map UserId CorrectionTableUserData , CryptoFileNameSubmission , Bool {- Access to non-anonymous submission data -} + , Maybe AuthorshipStatementSubmissionState ) @@ -135,6 +141,9 @@ resultCryptoID = _dbrOutput . _7 resultNonAnonymousAccess :: Lens' CorrectionTableData Bool resultNonAnonymousAccess = _dbrOutput . _8 +resultASState :: Lens' CorrectionTableData (Maybe AuthorshipStatementSubmissionState) +resultASState = _dbrOutput . _9 + -- Where Clauses ratedBy :: UserId -> CorrectionTableWhere @@ -291,6 +300,22 @@ colCommentField' l = sortable (Just "comment") (i18nCell MsgRatingComment) $ (ce colLastEdit :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) colLastEdit = sortable (Just "last-edit") (i18nCell MsgTableLastEdit) $ \x -> maybeCell (x ^? resultLastEdit) dateTimeCell +colAuthorshipStatementState :: forall m a. IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) +colAuthorshipStatementState = sortable (Just "as-state") (i18nCell MsgSubmissionUserAuthorshipStatementState) $ \x -> + let heatC :: AuthorshipStatementSubmissionState -> DBCell m a -> DBCell m a + heatC s c + = c + & cellAttrs %~ addAttrsClass "heated" + & cellAttrs <>~ pure ("style", [st|--hotness: #{tshow (boolHeat (s /= ASExists))}|]) + tid = x ^. resultCourseTerm + ssh = x ^. resultCourseSchool + csh = x ^. resultCourseShorthand + shn = x ^. resultSheet . _entityVal . _sheetName + cID = x ^. resultCryptoID + + asRoute = CSubmissionR tid ssh csh shn cID SubAuthorshipStatementsR + in maybeCell (x ^. resultASState) (\s -> heatC s $ anchorCell asRoute (i18n s :: Widget)) + filterUICourse :: Handler (OptionList Text) -> DBFilterUI filterUICourse courseOptions = flip (prismAForm $ singletonFilter "course") $ aopt (lift `hoistField` selectField courseOptions) (fslI MsgTableCourse) @@ -326,7 +351,7 @@ filterUIIsAssigned :: DBFilterUI filterUIIsAssigned = flip (prismAForm $ singletonFilter "isassigned" . maybePrism _PathPiece) $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableHasCorrector) filterUISubmissionGroup :: DBFilterUI -filterUISubmissionGroup = flip (prismAForm $ singletonFilter "submittors-group") $ aopt textField (fslI MsgTableSubmissionGroup) +filterUISubmissionGroup = flip (prismAForm $ singletonFilter "submission-group") $ aopt textField (fslI MsgTableSubmissionGroup) filterUIRating :: DBFilterUI filterUIRating = flip (prismAForm $ singletonFilter "rating" . maybePrism _PathPiece) $ aopt (lift `hoistField` pointsField) (fslI MsgColumnRatingPoints) @@ -334,6 +359,9 @@ filterUIRating = flip (prismAForm $ singletonFilter "rating" . maybePrism _PathP filterUIComment :: DBFilterUI filterUIComment mPrev = singletonMap "comment" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgRatingComment) (Just <$> listToMaybe =<< (Map.lookup "comment" =<< mPrev)) +filterUIAuthorshipStatementState :: DBFilterUI +filterUIAuthorshipStatementState = flip (prismAForm $ singletonFilter "as-state" . maybePrism _PathPiece) $ aopt (selectField' (Just $ SomeMessage MsgTableNoFilter) optionsFinite :: Field _ AuthorshipStatementSubmissionState) (fslI MsgSubmissionUserAuthorshipStatementState) + makeCorrectionsTable :: ( IsDBTable m x, ToSortable h, Functor h ) => CorrectionTableWhere -> Colonnade h CorrectionTableData (DBCell m x) -> _ -> PSValidator m x -> DBParams m x -> DB (DBResult m x) @@ -368,6 +396,13 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' psValidator dbtParams let haystack = map CI.mk . unpack $ toPathPiece cid in guard $ any (`isInfixOf` haystack) criteria + mASDefinition <- lift . lift . $cachedHereBinary shId $ getSheetAuthorshipStatement sheet + asState <- for mASDefinition $ \_ -> + lift . lift . $cachedHereBinary sId $ getSubmissionAuthorshipStatement mASDefinition sId + + forMM_ (preview $ _dbtProjFilter . _corrProjFilterAuthorshipStatementState . _Wrapped . _Just) $ \criterion -> + guard $ asState == Just criterion + submittors <- lift . lift . E.select . E.from $ \((submissionUser `E.InnerJoin` user) `E.LeftOuterJoin` pseudonym) -> do E.on $ pseudonym E.?. SheetPseudonymUser E.==. E.just (user E.^. UserId) E.&&. pseudonym E.?. SheetPseudonymSheet E.==. E.just (E.val shId) @@ -392,7 +427,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' psValidator dbtParams (return $ not sheetAnonymousCorrection) (hasReadAccessTo $ CourseR courseTerm courseSchool courseShorthand CCorrectionsR) - return (submission, sheet, (courseName, courseShorthand, courseTerm, courseSchool), mCorrector, mbLastEdit, submittorMap, cid, nonAnonymousAccess) + return (submission, sheet, (courseName, courseShorthand, courseTerm, courseSchool), mCorrector, mbLastEdit, submittorMap, cid, nonAnonymousAccess, asState) dbtRowKey = views querySubmission (E.^. SubmissionId) dbtSorting = mconcat [ singletonMap "term" . SortColumn $ views queryCourse (E.^. CourseTerm) @@ -418,7 +453,8 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' psValidator dbtParams , singletonMap "submittors-pseudonyms" . SortProjected . comparing $ \x -> setOf (resultSubmittors . resultUserPseudonym . re _PseudonymText) x , singletonMap "comment" . SortColumn $ views querySubmission (E.^. SubmissionRatingComment) -- sorting by comment specifically requested by correctors to easily see submissions to be done , singletonMap "last-edit" . SortColumn $ view queryLastEdit - , singletonMap "submission" . SortProjected . comparing $ toPathPiece . view resultCryptoID + , singletonMap "submission" . SortProjected . comparing $ views resultCryptoID toPathPiece + , singletonMap "as-state" . SortProjected . comparing $ view resultASState ] dbtFilter = mconcat [ singletonMap "term" . FilterColumn . E.mkExactFilter $ views queryCourse (E.^. CourseTerm) @@ -461,6 +497,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' psValidator dbtParams , singletonMap "comment" . FilterColumn . E.mkContainsFilterWith Just $ views querySubmission (E.^. SubmissionRatingComment) , singletonMap "submission" $ FilterProjected (_corrProjFilterSubmission ?~) , singletonMap "pseudonym" $ FilterProjected (_corrProjFilterPseudonym ?~) + , singletonMap "as-state" $ FilterProjected (_corrProjFilterAuthorshipStatementState <>~) ] dbtFilterUI = fromMaybe mempty dbtFilterUI' dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (const defaultDBSFilterLayout) dbtFilterUI' } @@ -742,31 +779,41 @@ postCorrectionsR = do getCCorrectionsR, postCCorrectionsR :: TermId -> SchoolId -> CourseShorthand -> Handler TypedContent getCCorrectionsR = postCCorrectionsR postCCorrectionsR tid ssh csh = do - Entity cid _ <- runDB $ getBy404 $ TermSchoolCourseShort tid ssh csh + (Entity cid _, doSubmissionGroups, doAuthorshipStatements) <- runDB $ do + course@(Entity cid _) <- getBy404 $ TermSchoolCourseShort tid ssh csh + doSubmissionGroups <- exists [SubmissionGroupCourse ==. cid] + doAuthorshipStatements <- runConduit $ + (E.selectSource . E.from $ \sheet -> sheet <$ E.where_ (sheet E.^. SheetCourse E.==. E.val cid)) + .| C.mapM getSheetAuthorshipStatement + .| C.map (is _Just) + .| C.or + return (course, doSubmissionGroups, doAuthorshipStatements) let whereClause :: CorrectionTableWhere whereClause = courseIs cid - colonnade = mconcat -- should match getSSubsR for consistent UX - [ colSelect - , colSheet - , colSMatrikel - , colSubmittors - , colSGroups - , colSubmissionLink - , colLastEdit - , colRating - , colRated - , colCorrector - , colAssigned + colonnade = mconcat $ catMaybes -- should match getSSubsR for consistent UX + [ pure colSelect + , pure colSheet + , pure colSMatrikel + , pure colSubmittors + , guardOn doSubmissionGroups colSGroups + , pure colSubmissionLink + , pure colLastEdit + , guardOn doAuthorshipStatements colAuthorshipStatementState + , pure colRating + , pure colRated + , pure colCorrector + , pure colAssigned ] -- Continue here filterUI = Just $ mconcat - [ filterUIUserNameEmail + [ filterUISheetSearch + , filterUIUserNameEmail , filterUIUserMatrikelnummer , filterUIPseudonym - , filterUISheetSearch + , filterUISubmissionGroup + , filterUIAuthorshipStatementState , filterUICorrectorNameEmail , filterUIIsAssigned , filterUIIsRated - , filterUISubmissionGroup , filterUISubmission ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway @@ -779,28 +826,35 @@ postCCorrectionsR tid ssh csh = do getSSubsR, postSSubsR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent getSSubsR = postSSubsR postSSubsR tid ssh csh shn = do - shid <- runDB $ fetchSheetId tid ssh csh shn + (shid, doSubmissionGroups, doAuthorshipStatements) <- runDB $ do + sheet@(Entity shid Sheet{..}) <- fetchSheet tid ssh csh shn + doSubmissionGroups <- exists [SubmissionGroupCourse ==. sheetCourse] + doAuthorshipStatements <- is _Just <$> getSheetAuthorshipStatement sheet + return (shid, doSubmissionGroups, doAuthorshipStatements) let whereClause :: CorrectionTableWhere whereClause = sheetIs shid - colonnade = mconcat -- should match getCCorrectionsR for consistent UX - [ colSelect - , colSMatrikel - , colSubmittors - , colSubmissionLink - , colLastEdit - , colRating - , colRated - , colCorrector - , colAssigned + colonnade = mconcat $ catMaybes -- should match getCCorrectionsR for consistent UX + [ pure colSelect + , pure colSMatrikel + , pure colSubmittors + , guardOn doSubmissionGroups colSGroups + , pure colSubmissionLink + , pure colLastEdit + , guardOn doAuthorshipStatements colAuthorshipStatementState + , pure colRating + , pure colRated + , pure colCorrector + , pure colAssigned ] filterUI = Just $ mconcat [ filterUIUserNameEmail , filterUIUserMatrikelnummer , filterUIPseudonym + , filterUISubmissionGroup + , filterUIAuthorshipStatementState , filterUICorrectorNameEmail , filterUIIsAssigned , filterUIIsRated - , filterUISubmissionGroup , filterUISubmission ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway diff --git a/src/Handler/Utils/Submission.hs b/src/Handler/Utils/Submission.hs index 1d5e5ab7a..96a0710f9 100644 --- a/src/Handler/Utils/Submission.hs +++ b/src/Handler/Utils/Submission.hs @@ -11,6 +11,8 @@ module Handler.Utils.Submission , submissionMatchesSheet , submissionDeleteRoute , correctionInvisibleWidget + , AuthorshipStatementSubmissionState(..) + , getUserAuthorshipStatement, getSubmissionAuthorshipStatement ) where import Import hiding (joinPath) @@ -36,6 +38,7 @@ import Handler.Utils import qualified Handler.Utils.Rating as Rating (extractRatings) import Handler.Utils.Delete +import Database.Persist.Sql (SqlBackendCanRead) import qualified Database.Esqueleto.Legacy as E import qualified Database.Esqueleto.Utils.TH as E @@ -976,3 +979,58 @@ correctionInvisibleWidget tid ssh csh shn cID (Entity subId sub) = runMaybeT $ d tellPoint CorrectionInvisibleExamUnfinished return $ notification NotificationBroad =<< messageIconWidget Warning IconInvisible $(widgetFile "submission-correction-invisible") + + +data AuthorshipStatementSubmissionState + = ASMissing + | ASOldStatement + | ASExists + deriving (Eq, Read, Show, Enum, Bounded, Generic, Typeable) + deriving anyclass (Universe, Finite) + +deriving stock instance Ord AuthorshipStatementSubmissionState -- ^ Larger roughly encodes better; summaries are taken with `max` + +nullaryPathPiece ''AuthorshipStatementSubmissionState $ camelToPathPiece' 1 + +embedRenderMessage ''UniWorX ''AuthorshipStatementSubmissionState $ concat . ("SubmissionAuthorshipStatementState" :) . drop 1 . splitCamel + + +getUserAuthorshipStatement :: ( MonadResource m + , IsSqlBackend backend, SqlBackendCanRead backend + ) + => Maybe (Entity AuthorshipStatementDefinition) -- ^ Currently expected authorship statement; see `getSheetAuthorshipStatement` + -> SubmissionId + -> UserId + -> ReaderT backend m AuthorshipStatementSubmissionState +getUserAuthorshipStatement mASDefinition subId uid = runConduit $ + getStmts + .| fmap toRes (execWriterC . C.mapM_ $ tell . toPoint) + where + getStmts = E.selectSource . E.from $ \authorshipStatementSubmission -> do + E.where_ $ authorshipStatementSubmission E.^. AuthorshipStatementSubmissionSubmission E.==. E.val subId + E.&&. authorshipStatementSubmission E.^. AuthorshipStatementSubmissionUser E.==. E.val uid + return authorshipStatementSubmission + toPoint :: Entity AuthorshipStatementSubmission -> Maybe Any + toPoint (Entity _ AuthorshipStatementSubmission{..}) = Just . Any $ fmap entityKey mASDefinition == Just authorshipStatementSubmissionStatement + toRes :: Maybe Any -> AuthorshipStatementSubmissionState + toRes = \case + Just (Any True) -> ASExists + Just (Any False) -> ASOldStatement + Nothing -> ASMissing + +getSubmissionAuthorshipStatement :: ( MonadResource m + , IsSqlBackend backend, SqlBackendCanRead backend + ) + => Maybe (Entity AuthorshipStatementDefinition) -- ^ Currently expected authorship statement; see `getSheetAuthorshipStatement` + -> SubmissionId + -> ReaderT backend m AuthorshipStatementSubmissionState +getSubmissionAuthorshipStatement mASDefinition subId = fmap (fromMaybe ASMissing) . runConduit $ + sourceSubmissionUsers + .| C.map E.unValue + .| C.mapM getUserAuthorshipStatement' + .| C.maximum + where + getUserAuthorshipStatement' = getUserAuthorshipStatement mASDefinition subId + sourceSubmissionUsers = E.selectSource . E.from $ \submissionUser -> do + E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val subId + return $ submissionUser E.^. SubmissionUserUser From 2a6248e3d5d4f4de5f1c7d6c6bcf092dc9873a2e Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Wed, 18 Aug 2021 16:54:50 +0200 Subject: [PATCH 016/143] feat(corrections-r): csv export Fixes #705 --- .../courses/submission/de-de-formal.msg | 33 ++- .../categories/courses/submission/en-eu.msg | 31 ++ src/Data/Scientific/Instances.hs | 4 +- src/Data/Word/Word24/Instances.hs | 7 + src/Foundation/I18n.hs | 2 + src/Handler/Course/User.hs | 10 +- src/Handler/Course/Users.hs | 10 +- src/Handler/Submission/Grade.hs | 2 +- src/Handler/Submission/List.hs | 269 ++++++++++++++++-- src/Handler/Utils/StudyFeatures.hs | 4 +- src/Handler/Utils/Submission.hs | 14 - src/Model/Types/DateTime.hs | 2 + src/Model/Types/Submission.hs | 23 ++ src/Utils/Csv.hs | 36 +++ ...corrections-csv-export.de-de-formal.hamlet | 2 + .../corrections-csv-export.en-eu.hamlet | 2 + test/Data/Scientific/InstancesSpec.hs | 10 + test/Utils/CsvSpec.hs | 38 +++ 18 files changed, 444 insertions(+), 55 deletions(-) create mode 100644 templates/i18n/changelog/corrections-csv-export.de-de-formal.hamlet create mode 100644 templates/i18n/changelog/corrections-csv-export.en-eu.hamlet create mode 100644 test/Data/Scientific/InstancesSpec.hs create mode 100644 test/Utils/CsvSpec.hs diff --git a/messages/uniworx/categories/courses/submission/de-de-formal.msg b/messages/uniworx/categories/courses/submission/de-de-formal.msg index 54a7795d5..b2b734946 100644 --- a/messages/uniworx/categories/courses/submission/de-de-formal.msg +++ b/messages/uniworx/categories/courses/submission/de-de-formal.msg @@ -228,4 +228,35 @@ SubmissionColumnAuthorshipStatementTime: Zeitstempel SubmissionColumnAuthorshipStatementWording: Wortlaut SubmissionFilterAuthorshipStatementCurrent: Aktueller Wortlaut -SubmissionNoUsers: Diese Abgabe hat keine assoziierten Benutzer! \ No newline at end of file +SubmissionNoUsers: Diese Abgabe hat keine assoziierten Benutzer! + +CsvColumnCorrectionTerm: Semester des Kurses der Abgabe +CsvColumnCorrectionSchool: Institut des Kurses der Abgabe +CsvColumnCorrectionCourse: Kürzel des Kurses der Abgabe +CsvColumnCorrectionSheet: Name des Übungsblatts der Abgabe +CsvColumnCorrectionSubmission: Nummer der Abgabe (uwa…) +CsvColumnCorrectionSurname: Nachnamen der Abgebenden als Semikolon (;) separierte Liste +CsvColumnCorrectionFirstName: Vornamen der Abgebenden als Semikolon (;) separierte Liste +CsvColumnCorrectionName: Volle Namen der Abgebenden als Semikolon (;) separierte Liste +CsvColumnCorrectionMatriculation: Matrikelnummern der Abgebenden als Semikolon (;) separierte Liste +CsvColumnCorrectionEmail: E-Mail Adressen der Abgebenden als Semikolon (;) separierte Liste +CsvColumnCorrectionPseudonym: Abgabe-Pseudonyme der Abgebenden als Semikolon (;) separierte Liste +CsvColumnCorrectionSubmissionGroup: Feste Abgabegruppen der Abgebenden als Semikolon (;) separierte Liste +CsvColumnCorrectionAuthorshipStatementState: Zustände der Eigenständigkeitserklärungen ("#{toPathPiece ASMissing}", "#{toPathPiece ASOldStatement}" oder "#{toPathPiece ASExists}") als Semikolon (;) separierte Liste +CsvColumnCorrectionCorrectorName: Voller Name des Korrektors der Abgabe +CsvColumnCorrectionCorrectorEmail: E-Mail Adresse des Korrektors der Abgabe +CsvColumnCorrectionRatingDone: Bewertung abgeschlossen ("t"/"f") +CsvColumnCorrectionRatedAt: Zeitpunkt der Bewertung (ISO 8601) +CsvColumnCorrectionAssigned: Zeitpunkt der Zuteilung des Korrektors (ISO 8601) +CsvColumnCorrectionLastEdit: Zeitpunkt der letzten Änderung der Abgabe (ISO 8601) +CsvColumnCorrectionRatingPoints: Erreichte Punktezahl (Für “_{MsgSheetGradingPassBinary}” entspricht 0 “_{MsgRatingNotPassed}” und alles andere “_{MsgRatingPassed}”) +CsvColumnCorrectionRatingComment: Bewertungskommentar + +CorrectionTableCsvNameSheetCorrections tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}-abgaben +CorrectionTableCsvSheetNameSheetCorrections tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn} Abgaben +CorrectionTableCsvNameCourseCorrections tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-abgaben +CorrectionTableCsvSheetNameCourseCorrections tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh} Abgaben +CorrectionTableCsvNameCorrections: abgaben +CorrectionTableCsvSheetNameCorrections: Abgaben +CorrectionTableCsvNameCourseUserCorrections tid@TermId ssh@SchoolId csh@CourseShorthand displayName@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase displayName}-abgaben +CorrectionTableCsvSheetNameCourseUserCorrections tid@TermId ssh@SchoolId csh@CourseShorthand displayName@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase displayName} Abgaben \ No newline at end of file diff --git a/messages/uniworx/categories/courses/submission/en-eu.msg b/messages/uniworx/categories/courses/submission/en-eu.msg index e7f96147c..1e9adbd3b 100644 --- a/messages/uniworx/categories/courses/submission/en-eu.msg +++ b/messages/uniworx/categories/courses/submission/en-eu.msg @@ -228,3 +228,34 @@ SubmissionColumnAuthorshipStatementWording: Wording SubmissionFilterAuthorshipStatementCurrent: Current wording SubmissionNoUsers: This submission has no associated users! + +CsvColumnCorrectionTerm: Term of the course of the submission +CsvColumnCorrectionSchool: School of the course of the submission +CsvColumnCorrectionCourse: Shorthand of the course of the submission +CsvColumnCorrectionSheet: Name of the sheet of the submission +CsvColumnCorrectionSubmission: Number of the submission (uwa…) +CsvColumnCorrectionSurname: Submittor's surnames, separated by semicolon (;) +CsvColumnCorrectionFirstName: Submittor's first names, separated by semicolon (;) +CsvColumnCorrectionName: Submittor's full names, separated by semicolon (;) +CsvColumnCorrectionMatriculation: Submittor's matriculations, separated by semicolon (;) +CsvColumnCorrectionEmail: Submittor's email addresses, separated by semicolon (;) +CsvColumnCorrectionPseudonym: Submittor's submission pseudonyms, separated by semicolon (;) +CsvColumnCorrectionSubmissionGroup: Submittor's submisson groups, separated by semicolon (;) +CsvColumnCorrectionAuthorshipStatementState: States of the statements of authorship ("#{toPathPiece ASMissing}", "#{toPathPiece ASOldStatement}", or "#{toPathPiece ASExists}"), separated by semicolon (;) +CsvColumnCorrectionCorrectorName: Full name of the corrector of the submission +CsvColumnCorrectionCorrectorEmail: Email address of the corrector of the submission +CsvColumnCorrectionRatingDone: Rating done ("t"/"f") +CsvColumnCorrectionRatedAt: Timestamp of rating (ISO 8601) +CsvColumnCorrectionAssigned: Timestamp of when corrector was assigned (ISO 8601) +CsvColumnCorrectionLastEdit: Timestamp of the last edit of the submission (ISO 8601) +CsvColumnCorrectionRatingPoints: Achieved points (for “_{MsgSheetGradingPassBinary}” 0 means “_{MsgRatingNotPassed}”, everything else means “_{MsgRatingPassed}”) +CsvColumnCorrectionRatingComment: Rating comment + +CorrectionTableCsvNameSheetCorrections tid ssh csh shn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}-submissions +CorrectionTableCsvSheetNameSheetCorrections tid ssh csh shn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn} Submissions +CorrectionTableCsvNameCourseCorrections tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-submissions +CorrectionTableCsvSheetNameCourseCorrections tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh} Submissions +CorrectionTableCsvNameCorrections: submissions +CorrectionTableCsvSheetNameCorrections: Submissions +CorrectionTableCsvNameCourseUserCorrections tid ssh csh displayName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase displayName}-submissions +CorrectionTableCsvSheetNameCourseUserCorrections tid ssh csh displayName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase displayName} Submissions diff --git a/src/Data/Scientific/Instances.hs b/src/Data/Scientific/Instances.hs index cee91482d..8c0c83e89 100644 --- a/src/Data/Scientific/Instances.hs +++ b/src/Data/Scientific/Instances.hs @@ -9,7 +9,9 @@ import Data.Scientific import Web.PathPieces +import Text.ParserCombinators.ReadP (readP_to_S) + instance PathPiece Scientific where toPathPiece = pack . formatScientific Fixed Nothing - fromPathPiece = readFromPathPiece + fromPathPiece = fmap fst . listToMaybe . filter (\(_, rStr) -> null rStr) . readP_to_S scientificP . unpack diff --git a/src/Data/Word/Word24/Instances.hs b/src/Data/Word/Word24/Instances.hs index e1d6add1a..b80cdc620 100644 --- a/src/Data/Word/Word24/Instances.hs +++ b/src/Data/Word/Word24/Instances.hs @@ -12,6 +12,8 @@ import System.Random (Random(..)) import Data.Aeson (FromJSON(..), ToJSON(..)) import qualified Data.Aeson.Types as Aeson +import Web.PathPieces + import Data.Word.Word24 import Control.Lens @@ -19,6 +21,7 @@ import Control.Lens import Control.Monad.Fail import qualified Data.Scientific as Scientific +import Data.Scientific.Instances () import Data.Binary import Data.Bits @@ -51,6 +54,10 @@ instance FromJSON Word24 where instance ToJSON Word24 where toJSON = Aeson.Number . fromIntegral +instance PathPiece Word24 where + toPathPiece p = toPathPiece (fromIntegral p :: Word32) + fromPathPiece = Scientific.toBoundedInteger <=< fromPathPiece + -- | Big Endian instance Binary Word24 where diff --git a/src/Foundation/I18n.hs b/src/Foundation/I18n.hs index 9dc051554..f85cc309a 100644 --- a/src/Foundation/I18n.hs +++ b/src/Foundation/I18n.hs @@ -308,6 +308,8 @@ embedRenderMessageVariant ''UniWorX ''ADInvalidCredentials ("InvalidCredentials" embedRenderMessage ''UniWorX ''SchoolAuthorshipStatementMode id embedRenderMessage ''UniWorX ''SheetAuthorshipStatementMode id +embedRenderMessage ''UniWorX ''AuthorshipStatementSubmissionState $ concat . ("SubmissionAuthorshipStatementState" :) . drop 1 . splitCamel + newtype ShortSex = ShortSex Sex embedRenderMessageVariant ''UniWorX ''ShortSex ("Short" <>) diff --git a/src/Handler/Course/User.hs b/src/Handler/Course/User.hs index 5db9da78b..7ef122422 100644 --- a/src/Handler/Course/User.hs +++ b/src/Handler/Course/User.hs @@ -240,7 +240,7 @@ courseUserNoteSection (Entity cid Course{..}) (Entity uid _) = do courseUserSubmissionsSection :: Entity Course -> Entity User -> MaybeT Handler Widget -courseUserSubmissionsSection (Entity cid Course{..}) (Entity uid _) = do +courseUserSubmissionsSection (Entity cid Course{..}) (Entity uid User{..}) = do guardM . lift . hasWriteAccessTo $ CourseR courseTerm courseSchool courseShorthand CCorrectionsR let whereClause :: CorrectionTableWhere @@ -268,7 +268,13 @@ courseUserSubmissionsSection (Entity cid Course{..}) (Entity uid _) = do , filterUISubmission ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway - (cWdgt, statistics) <- lift . correctionsR' whereClause colonnade filterUI psValidator $ Map.fromList + csvSettings = Just CorrectionTableCsvSettings + { cTableCsvQualification = CorrectionTableCsvQualifySheet + , cTableCsvName = MsgCorrectionTableCsvNameCourseUserCorrections courseTerm courseSchool courseShorthand userDisplayName + , cTableCsvSheetName = MsgCorrectionTableCsvSheetNameCourseUserCorrections courseTerm courseSchool courseShorthand userDisplayName + , cTableShowCorrector = True + } + (cWdgt, statistics) <- lift . correctionsR' whereClause colonnade filterUI csvSettings psValidator $ Map.fromList [ downloadAction , assignAction (Left cid) , deleteAction diff --git a/src/Handler/Course/Users.hs b/src/Handler/Course/Users.hs index 2a12a905c..0d25a488b 100644 --- a/src/Handler/Course/Users.hs +++ b/src/Handler/Course/Users.hs @@ -197,17 +197,13 @@ instance Csv.ToNamedRecord UserTableCsv where , "email" Csv..= csvUserEmail , "study-features" Csv..= csvUserStudyFeatures , "submission-group" Csv..= csvUserSubmissionGroup - ] ++ - [ let tutsStr = Text.intercalate "; " . map CI.original $ csvUserTutorials ^. _1 - in "tutorial" Csv..= tutsStr + , "tutorial" Csv..= CsvSemicolonList (csvUserTutorials ^. _1) ] ++ [ encodeUtf8 (CI.foldedCase regGroup) Csv..= (CI.original <$> mTut) | (regGroup, mTut) <- Map.toList $ csvUserTutorials ^. _2 ] ++ - [ let examsStr = Text.intercalate "; " $ map CI.original csvUserExams - in "exams" Csv..= examsStr - ] ++ - [ "registration" Csv..= csvUserRegistration + [ "exams" Csv..= CsvSemicolonList csvUserExams + , "registration" Csv..= csvUserRegistration ] ++ [ encodeUtf8 (CI.foldedCase shn) Csv..= res | (shn, res) <- Map.toList csvUserSheets diff --git a/src/Handler/Submission/Grade.hs b/src/Handler/Submission/Grade.hs index 1ddb8019e..d805b574e 100644 --- a/src/Handler/Submission/Grade.hs +++ b/src/Handler/Submission/Grade.hs @@ -64,7 +64,7 @@ postCorrectionsGradeR = do & defaultSorting [SortDescBy "ratingtime"] :: PSValidator (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult SubmissionId (Bool, Maybe Points, Maybe Text) CorrectionTableData)) unFormResult = getDBFormResult $ \(view $ resultSubmission . _entityVal -> sub@Submission{..}) -> (submissionRatingDone sub, submissionRatingPoints, submissionRatingComment) - (fmap unFormResult -> tableRes, table) <- runDB $ makeCorrectionsTable whereClause displayColumns filterUI psValidator $ def + (fmap unFormResult -> tableRes, table) <- runDB $ makeCorrectionsTable whereClause displayColumns filterUI Nothing psValidator $ def { dbParamsFormAction = Just $ SomeRoute CorrectionsGradeR } diff --git a/src/Handler/Submission/List.hs b/src/Handler/Submission/List.hs index 14f1fdb29..f1ded3f63 100644 --- a/src/Handler/Submission/List.hs +++ b/src/Handler/Submission/List.hs @@ -14,6 +14,7 @@ module Handler.Submission.List , makeCorrectionsTable , CorrectionTableData, CorrectionTableWhere , ActionCorrections(..), downloadAction, deleteAction, assignAction, autoAssignAction + , CorrectionTableCsvQualification(..), CorrectionTableCsvSettings(..) ) where import Import hiding (link) @@ -23,7 +24,6 @@ import Handler.Utils.Submission import Handler.Utils.SheetType import Handler.Utils.Delete -import Data.List as List (foldr) import qualified Data.Set as Set import qualified Data.Map.Strict as Map @@ -42,6 +42,8 @@ import Database.Persist.Sql (updateWhereCount) import Data.List (genericLength) +import qualified Data.Csv as Csv + data CorrectionTableFilterProj = CorrectionTableFilterProj { corrProjFilterSubmission :: Maybe (Set [CI Char]) @@ -66,7 +68,7 @@ type CorrectionTableExpr = ( E.SqlExpr (Entity Course) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User)) type CorrectionTableWhere = forall m. MonadReader CorrectionTableExpr m => m (E.SqlExpr (E.Value Bool)) type CorrectionTableCourseData = (CourseName, CourseShorthand, TermId, SchoolId) -type CorrectionTableUserData = (User, Maybe Pseudonym, Maybe SubmissionGroupName) +type CorrectionTableUserData = (User, Maybe Pseudonym, Maybe SubmissionGroupName, Maybe AuthorshipStatementSubmissionState) type CorrectionTableData = DBRow ( Entity Submission , Entity Sheet , CorrectionTableCourseData @@ -135,6 +137,9 @@ resultUserPseudonym = _2 . _Just resultUserSubmissionGroup :: Traversal' CorrectionTableUserData SubmissionGroupName resultUserSubmissionGroup = _3 . _Just +resultUserAuthorshipStatementState :: Traversal' CorrectionTableUserData AuthorshipStatementSubmissionState +resultUserAuthorshipStatementState = _4 . _Just + resultCryptoID :: Lens' CorrectionTableData CryptoFileNameSubmission resultCryptoID = _dbrOutput . _7 @@ -145,6 +150,159 @@ resultASState :: Lens' CorrectionTableData (Maybe AuthorshipStatementSubmissionS resultASState = _dbrOutput . _9 +data CorrectionTableCsv = CorrectionTableCsv + { csvCorrectionTerm :: Maybe TermIdentifier + , csvCorrectionSchool :: Maybe SchoolShorthand + , csvCorrectionCourse :: Maybe CourseShorthand + , csvCorrectionSheet :: Maybe SheetName + , csvCorrectionSubmission :: Maybe (CI Text) + , csvCorrectionLastEdit :: Maybe UTCTime + , csvCorrectionSurname :: Maybe [Maybe UserSurname] + , csvCorrectionFirstName :: Maybe [Maybe UserFirstName] + , csvCorrectionName :: Maybe [Maybe UserDisplayName] + , csvCorrectionMatriculation :: Maybe [Maybe UserMatriculation] + , csvCorrectionEmail :: Maybe [Maybe UserEmail] + , csvCorrectionPseudonym :: Maybe [Maybe Pseudonym] + , csvCorrectionSubmissionGroup :: Maybe [Maybe SubmissionGroupName] + , csvCorrectionAuthorshipStatementState :: Maybe [Maybe AuthorshipStatementSubmissionState] + , csvCorrectionAssigned :: Maybe UTCTime + , csvCorrectionCorrectorName :: Maybe UserDisplayName + , csvCorrectionCorrectorEmail :: Maybe UserEmail + , csvCorrectionRatingDone :: Maybe Bool + , csvCorrectionRatedAt :: Maybe UTCTime + , csvCorrectionRatingPoints :: Maybe Points + , csvCorrectionRatingComment :: Maybe Text + } deriving (Generic) +makeLenses_ ''CorrectionTableCsv + +correctionTableCsvOptions :: Csv.Options +correctionTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 2 } + +instance Csv.ToNamedRecord CorrectionTableCsv where + toNamedRecord CorrectionTableCsv{..} = Csv.namedRecord + [ "term" Csv..= csvCorrectionTerm + , "school" Csv..= csvCorrectionSchool + , "course" Csv..= csvCorrectionCourse + , "sheet" Csv..= csvCorrectionSheet + , "submission" Csv..= csvCorrectionSubmission + , "last-edit" Csv..= csvCorrectionLastEdit + , "surname" Csv..= maybe mempty (Csv.toField . CsvSemicolonList) csvCorrectionSurname + , "first-name" Csv..= maybe mempty (Csv.toField . CsvSemicolonList) csvCorrectionFirstName + , "name" Csv..= maybe mempty (Csv.toField . CsvSemicolonList) csvCorrectionName + , "matriculation" Csv..= maybe mempty (Csv.toField . CsvSemicolonList . mkEmpty) csvCorrectionMatriculation + , "email" Csv..= maybe mempty (Csv.toField . CsvSemicolonList) csvCorrectionEmail + , "pseudonym" Csv..= maybe mempty (Csv.toField . CsvSemicolonList . mkEmpty) csvCorrectionPseudonym + , "submission-group" Csv..= maybe mempty (Csv.toField . CsvSemicolonList . mkEmpty) csvCorrectionSubmissionGroup + , "authorship-statement-state" Csv..= maybe mempty (Csv.toField . CsvSemicolonList . mkEmpty) csvCorrectionAuthorshipStatementState + , "assigned" Csv..= csvCorrectionAssigned + , "corrector-name" Csv..= csvCorrectionCorrectorName + , "corrector-email" Csv..= csvCorrectionCorrectorEmail + , "rating-done" Csv..= csvCorrectionRatingDone + , "rated-at" Csv..= csvCorrectionRatedAt + , "rating-points" Csv..= csvCorrectionRatingPoints + , "rating-comment" Csv..= csvCorrectionRatingComment + ] + where + mkEmpty = \case + [Nothing] -> [] + x -> x + +instance Csv.DefaultOrdered CorrectionTableCsv where + headerOrder = Csv.genericHeaderOrder correctionTableCsvOptions + +instance Csv.FromNamedRecord CorrectionTableCsv where + parseNamedRecord csv + = CorrectionTableCsv + <$> csv .:?? "term" + <*> csv .:?? "school" + <*> csv .:?? "course" + <*> csv .:?? "sheet" + <*> csv .:?? "submission" + <*> csv .:?? "last-edit" + <*> fmap (fmap unCsvSemicolonList) (csv .:?? "surname") + <*> fmap (fmap unCsvSemicolonList) (csv .:?? "first-name") + <*> fmap (fmap unCsvSemicolonList) (csv .:?? "name") + <*> fmap (fmap unCsvSemicolonList) (csv .:?? "matriculation") + <*> fmap (fmap unCsvSemicolonList) (csv .:?? "email") + <*> fmap (fmap unCsvSemicolonList) (csv .:?? "pseudonym") + <*> fmap (fmap unCsvSemicolonList) (csv .:?? "submission-group") + <*> fmap (fmap unCsvSemicolonList) (csv .:?? "authorship-statement-state") + <*> csv .:?? "assigned" + <*> csv .:?? "corrector-name" + <*> csv .:?? "corrector-email" + <*> csv .:?? "rating-done" + <*> csv .:?? "rated-at" + <*> csv .:?? "rating-points" + <*> csv .:?? "rating-comment" + +instance CsvColumnsExplained CorrectionTableCsv where + csvColumnsExplanations = genericCsvColumnsExplanations correctionTableCsvOptions $ Map.fromList + [ ('csvCorrectionTerm , MsgCsvColumnCorrectionTerm) + , ('csvCorrectionSchool , MsgCsvColumnCorrectionSchool) + , ('csvCorrectionCourse , MsgCsvColumnCorrectionCourse) + , ('csvCorrectionSheet , MsgCsvColumnCorrectionSheet) + , ('csvCorrectionSubmission , MsgCsvColumnCorrectionSubmission) + , ('csvCorrectionLastEdit , MsgCsvColumnCorrectionLastEdit) + , ('csvCorrectionSurname , MsgCsvColumnCorrectionSurname) + , ('csvCorrectionFirstName , MsgCsvColumnCorrectionFirstName) + , ('csvCorrectionName , MsgCsvColumnCorrectionName) + , ('csvCorrectionMatriculation , MsgCsvColumnCorrectionMatriculation) + , ('csvCorrectionEmail , MsgCsvColumnCorrectionEmail) + , ('csvCorrectionPseudonym , MsgCsvColumnCorrectionPseudonym) + , ('csvCorrectionSubmissionGroup, MsgCsvColumnCorrectionSubmissionGroup) + , ('csvCorrectionAuthorshipStatementState, MsgCsvColumnCorrectionAuthorshipStatementState) + , ('csvCorrectionAssigned , MsgCsvColumnCorrectionAssigned) + , ('csvCorrectionCorrectorName , MsgCsvColumnCorrectionCorrectorName) + , ('csvCorrectionCorrectorEmail , MsgCsvColumnCorrectionCorrectorEmail) + , ('csvCorrectionRatingDone , MsgCsvColumnCorrectionRatingDone) + , ('csvCorrectionRatedAt , MsgCsvColumnCorrectionRatedAt) + , ('csvCorrectionRatingPoints , MsgCsvColumnCorrectionRatingPoints) + , ('csvCorrectionRatingComment , MsgCsvColumnCorrectionRatingComment) + ] + +data CorrectionTableCsvQualification + = CorrectionTableCsvNoQualification + | CorrectionTableCsvQualifySheet + | CorrectionTableCsvQualifyCourse + deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) + deriving anyclass (Universe, Finite) + +correctionTableCsvHeader :: Bool -- ^ @showCorrector@ + -> CorrectionTableCsvQualification -> Csv.Header +correctionTableCsvHeader showCorrector qual = Csv.header $ catMaybes + [ guardOn (qual >= CorrectionTableCsvQualifyCourse) "term" + , guardOn (qual >= CorrectionTableCsvQualifyCourse) "school" + , guardOn (qual >= CorrectionTableCsvQualifyCourse) "course" + , guardOn (qual >= CorrectionTableCsvQualifySheet) "sheet" + , pure "submission" + , pure "last-edit" + , pure "surname" + , pure "first-name" + , pure "name" + , pure "matriculation" + , pure "email" + , pure "pseudonym" + , pure "submission-group" + , pure "authorship-statement-state" + , pure "assigned" + , guardOn showCorrector "corrector-name" + , guardOn showCorrector "corrector-email" + , pure "rating-done" + , pure "rated-at" + , pure "rating-points" + , pure "rating-comment" + ] + +data CorrectionTableCsvSettings = forall filename sheetName. + ( RenderMessage UniWorX filename, RenderMessage UniWorX sheetName + ) => CorrectionTableCsvSettings + { cTableCsvQualification :: CorrectionTableCsvQualification + , cTableCsvName :: filename + , cTableCsvSheetName :: sheetName + , cTableShowCorrector :: Bool + } + + -- Where Clauses ratedBy :: UserId -> CorrectionTableWhere ratedBy uid = views querySubmission $ (E.==. E.justVal uid) . (E.^. SubmissionRatingBy) @@ -206,10 +364,10 @@ colSubmittors = sortable (Just "submittors") (i18nCell MsgSubmissionUsers) $ \x ssh = x ^. resultCourseSchool csh = x ^. resultCourseShorthand link uCID = CourseR tid ssh csh $ CUserR uCID - protoCell = listCell (sortOn (view $ _2 . resultUserUser . $(multifocusG 2) _userSurname _userDisplayName) $ itoListOf resultSubmittors x) $ \((encrypt -> mkUCID), u) -> + protoCell = listCell (sortOn (view $ _2 . resultUserUser . $(multifocusG 2) _userSurname _userDisplayName) $ itoListOf resultSubmittors x) $ \(encrypt -> mkUCID, u) -> let User{..} = u ^. resultUserUser mPseudo = u ^? resultUserPseudonym - in anchorCellCM $cacheIdentHere (link <$> mkUCID) $ + in anchorCellCM $cacheIdentHere (link <$> mkUCID) [whamlet| $newline never ^{nameWidget userDisplayName userSurname} @@ -298,7 +456,7 @@ colCommentField' l = sortable (Just "comment") (i18nCell MsgRatingComment) $ (ce (\(view (resultSubmission . _entityVal) -> Submission{..}) mkUnique -> over (_1.mapped) ((l .~) . assertM (not . null) . fmap (Text.strip . unTextarea)) . over _2 fvWidget <$> mopt textareaField (fsUniq mkUnique "comment") (Just $ Textarea <$> submissionRatingComment)) colLastEdit :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) -colLastEdit = sortable (Just "last-edit") (i18nCell MsgTableLastEdit) $ \x -> maybeCell (x ^? resultLastEdit) dateTimeCell +colLastEdit = sortable (Just "last-edit") (i18nCell MsgTableLastEdit) $ \x -> maybeCell (guardOnM (x ^. resultNonAnonymousAccess) $ x ^? resultLastEdit) dateTimeCell colAuthorshipStatementState :: forall m a. IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) colAuthorshipStatementState = sortable (Just "as-state") (i18nCell MsgSubmissionUserAuthorshipStatementState) $ \x -> @@ -314,7 +472,7 @@ colAuthorshipStatementState = sortable (Just "as-state") (i18nCell MsgSubmission cID = x ^. resultCryptoID asRoute = CSubmissionR tid ssh csh shn cID SubAuthorshipStatementsR - in maybeCell (x ^. resultASState) (\s -> heatC s $ anchorCell asRoute (i18n s :: Widget)) + in maybeCell (guardOnM (x ^. resultNonAnonymousAccess) $ x ^. resultASState) (\s -> heatC s $ anchorCell asRoute (i18n s :: Widget)) filterUICourse :: Handler (OptionList Text) -> DBFilterUI @@ -364,8 +522,8 @@ filterUIAuthorshipStatementState = flip (prismAForm $ singletonFilter "as-state" makeCorrectionsTable :: ( IsDBTable m x, ToSortable h, Functor h ) - => CorrectionTableWhere -> Colonnade h CorrectionTableData (DBCell m x) -> _ -> PSValidator m x -> DBParams m x -> DB (DBResult m x) -makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' psValidator dbtParams + => CorrectionTableWhere -> Colonnade h CorrectionTableData (DBCell m x) -> _ -> Maybe CorrectionTableCsvSettings -> PSValidator m x -> DBParams m x -> DB (DBResult m x) +makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' mCSVSettings psValidator dbtParams = let dbtSQLQuery = runReaderT $ do course <- view queryCourse sheet <- view querySheet @@ -396,12 +554,6 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' psValidator dbtParams let haystack = map CI.mk . unpack $ toPathPiece cid in guard $ any (`isInfixOf` haystack) criteria - mASDefinition <- lift . lift . $cachedHereBinary shId $ getSheetAuthorshipStatement sheet - asState <- for mASDefinition $ \_ -> - lift . lift . $cachedHereBinary sId $ getSubmissionAuthorshipStatement mASDefinition sId - - forMM_ (preview $ _dbtProjFilter . _corrProjFilterAuthorshipStatementState . _Wrapped . _Just) $ \criterion -> - guard $ asState == Just criterion submittors <- lift . lift . E.select . E.from $ \((submissionUser `E.InnerJoin` user) `E.LeftOuterJoin` pseudonym) -> do E.on $ pseudonym E.?. SheetPseudonymUser E.==. E.just (user E.^. UserId) @@ -416,8 +568,15 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' psValidator dbtParams return . E.just $ submissionGroup E.^. SubmissionGroupName return (user, pseudonym E.?. SheetPseudonymPseudonym, submissionGroup') - let - submittorMap = List.foldr (\(Entity userId user, E.Value pseudo, E.Value sGroup) -> Map.insert userId (user, pseudo, sGroup)) Map.empty submittors + + mASDefinition <- lift . lift . $cachedHereBinary shId $ getSheetAuthorshipStatement sheet + (submittorMap, fmap getMax -> asState) <- runWriterT . flip foldMapM submittors $ \(Entity userId user, E.Value pseudo, E.Value sGroup) -> do + asState <- for mASDefinition $ \_ -> lift . lift . lift $ getUserAuthorshipStatement mASDefinition sId userId + tell $ Max <$> asState + return $ Map.singleton userId (user, pseudo, sGroup, asState) + + forMM_ (preview $ _dbtProjFilter . _corrProjFilterAuthorshipStatementState . _Wrapped . _Just) $ \criterion -> + guard $ asState == Just criterion forMM_ (view $ _dbtProjFilter . _corrProjFilterPseudonym) $ \criteria -> let haystacks = setOf (folded . resultUserPseudonym . re _PseudonymText . to (map CI.mk . unpack)) submittorMap @@ -502,7 +661,41 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' psValidator dbtParams dbtFilterUI = fromMaybe mempty dbtFilterUI' dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (const defaultDBSFilterLayout) dbtFilterUI' } dbtIdent = "corrections" :: Text - dbtCsvEncode = noCsvEncode + dbtCsvEncode = do + CorrectionTableCsvSettings{..} <- mCSVSettings + return DBTCsvEncode + { dbtCsvExportForm = pure () + , dbtCsvNoExportData = Just id + , dbtCsvDoEncode = \() -> awaitForever $ \(_, row) -> runReaderC row $ do + submittors <- asks $ sortOn (view $ resultUserUser . $(multifocusG 2) _userSurname _userDisplayName ) . toListOf resultSubmittors + forM_ (bool pure (map pure) False submittors) $ \submittors' -> transPipe (withReaderT (, submittors')) $ do + let guardNonAnonymous = runMaybeT . guardMOnM (view $ _1 . resultNonAnonymousAccess) . MaybeT + yieldM $ CorrectionTableCsv + <$> preview (_1 . resultCourseTerm . _TermId) + <*> preview (_1 . resultCourseSchool . _SchoolId) + <*> preview (_1 . resultCourseShorthand) + <*> preview (_1 . resultSheet . _entityVal . _sheetName) + <*> preview (_1 . resultCryptoID . re (_CI . _PathPiece)) + <*> guardNonAnonymous (preview $ _1 . resultLastEdit) + <*> guardNonAnonymous (previews _2 (toListOf $ folded . resultUserUser . _userSurname . re _Just)) + <*> guardNonAnonymous (previews _2 (toListOf $ folded . resultUserUser . _userFirstName . re _Just)) + <*> guardNonAnonymous (previews _2 (toListOf $ folded . resultUserUser . _userDisplayName . re _Just)) + <*> guardNonAnonymous (previews _2 (toListOf $ folded . resultUserUser . _userMatrikelnummer)) + <*> guardNonAnonymous (previews _2 (toListOf $ folded . resultUserUser . _userEmail . re _Just)) + <*> guardNonAnonymous (previews _2 (toListOf $ folded . pre resultUserPseudonym)) + <*> guardNonAnonymous (previews _2 (toListOf $ folded . pre resultUserSubmissionGroup)) + <*> guardNonAnonymous (previews _2 (toListOf $ folded . pre resultUserAuthorshipStatementState)) + <*> preview (_1 . resultSubmission . _entityVal . _submissionRatingAssigned . _Just) + <*> preview (_1 . resultCorrector . _entityVal . _userDisplayName) + <*> preview (_1 . resultCorrector . _entityVal . _userEmail) + <*> preview (_1 . resultSubmission . _entityVal . to submissionRatingDone) + <*> preview (_1 . resultSubmission . _entityVal . _submissionRatingTime . _Just) + <*> preview (_1 . resultSubmission . _entityVal . _submissionRatingPoints . _Just) + <*> preview (_1 . resultSubmission . _entityVal . _submissionRatingComment . _Just) + , dbtCsvName = cTableCsvName, dbtCsvSheetName = cTableCsvSheetName + , dbtCsvHeader = \_ -> return $ correctionTableCsvHeader cTableShowCorrector cTableCsvQualification + , dbtCsvExampleData = Nothing + } dbtCsvDecode = Nothing dbtExtraReps = [] in dbTable psValidator DBTable{..} @@ -524,16 +717,16 @@ data ActionCorrectionsData = CorrDownloadData SubmissionDownloadAnonymous Submis | CorrAutoSetCorrectorData SheetId | CorrDeleteData -correctionsR :: CorrectionTableWhere -> _ -> _ -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler TypedContent -correctionsR whereClause displayColumns dbtFilterUI psValidator actions = do - (table, statistics) <- correctionsR' whereClause displayColumns dbtFilterUI psValidator actions +correctionsR :: CorrectionTableWhere -> _ -> _ -> Maybe CorrectionTableCsvSettings -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler TypedContent +correctionsR whereClause displayColumns dbtFilterUI csvSettings psValidator actions = do + (table, statistics) <- correctionsR' whereClause displayColumns dbtFilterUI csvSettings psValidator actions fmap toTypedContent . defaultLayout $ do setTitleI MsgCourseCorrectionsTitle $(widgetFile "corrections") -correctionsR' :: CorrectionTableWhere -> _ -> _ -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler (Widget, SheetTypeSummary SqlBackendKey) -correctionsR' whereClause displayColumns dbtFilterUI psValidator actions = do +correctionsR' :: CorrectionTableWhere -> _ -> _ -> Maybe CorrectionTableCsvSettings -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler (Widget, SheetTypeSummary SqlBackendKey) +correctionsR' whereClause displayColumns dbtFilterUI csvSettings psValidator actions = do currentRoute <- fromMaybe (error "correctionsR called from 404-handler") <$> getCurrentRoute -- This should never be called from a 404 handler postDeleteR $ \drRecords -> (submissionDeleteRoute drRecords) @@ -542,7 +735,7 @@ correctionsR' whereClause displayColumns dbtFilterUI psValidator actions = do } ((actionRes', statistics), table) <- runDB $ - makeCorrectionsTable whereClause displayColumns dbtFilterUI psValidator DBParamsForm + makeCorrectionsTable whereClause displayColumns dbtFilterUI csvSettings psValidator DBParamsForm { dbParamsFormMethod = POST , dbParamsFormAction = Just $ SomeRoute currentRoute , dbParamsFormAttrs = [] @@ -682,7 +875,12 @@ restrictAnonymous :: PSValidator m x -> PSValidator m x restrictAnonymous = restrictFilter (\k _ -> k /= "user-matriclenumber") . restrictFilter (\k _ -> k /= "user-name-email") . restrictFilter (\k _ -> k /= "submission-group") + . restrictFilter (\k _ -> k /= "as-state") + . restrictSorting (\k _ -> k /= "submittors") + . restrictSorting (\k _ -> k /= "submittors-matriculation") + . restrictSorting (\k _ -> k /= "submittors-group") . restrictSorting (\k _ -> k /= "last-edit") + . restrictSorting (\k _ -> k /= "as-state") restrictCorrector :: PSValidator m x -> PSValidator m x restrictCorrector = restrictFilter (\k _ -> k /= "corrector") @@ -772,7 +970,14 @@ postCorrectionsR = do & restrictAnonymous & defaultSorting [SortDescBy "ratingtime", SortAscBy "assignedtime" ] & defaultFilter (singletonMap "israted" [toPathPiece False]) - correctionsR whereClause colonnade filterUI psValidator $ Map.fromList + + csvSettings = Just CorrectionTableCsvSettings + { cTableCsvQualification = CorrectionTableCsvQualifyCourse + , cTableCsvName = MsgCorrectionTableCsvNameCorrections + , cTableCsvSheetName = MsgCorrectionTableCsvSheetNameCorrections + , cTableShowCorrector = False + } + correctionsR whereClause colonnade filterUI csvSettings psValidator $ Map.fromList [ downloadAction ] @@ -817,7 +1022,13 @@ postCCorrectionsR tid ssh csh = do , filterUISubmission ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway - correctionsR whereClause colonnade filterUI psValidator $ Map.fromList + csvSettings = Just CorrectionTableCsvSettings + { cTableCsvQualification = CorrectionTableCsvQualifySheet + , cTableCsvName = MsgCorrectionTableCsvNameCourseCorrections tid ssh csh + , cTableCsvSheetName = MsgCorrectionTableCsvSheetNameCourseCorrections tid ssh csh + , cTableShowCorrector = True + } + correctionsR whereClause colonnade filterUI csvSettings psValidator $ Map.fromList [ downloadAction , assignAction (Left cid) , deleteAction @@ -858,7 +1069,13 @@ postSSubsR tid ssh csh shn = do , filterUISubmission ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway - correctionsR whereClause colonnade filterUI psValidator $ Map.fromList + csvSettings = Just CorrectionTableCsvSettings + { cTableCsvQualification = CorrectionTableCsvNoQualification + , cTableCsvName = MsgCorrectionTableCsvNameSheetCorrections tid ssh csh shn + , cTableCsvSheetName = MsgCorrectionTableCsvSheetNameSheetCorrections tid ssh csh shn + , cTableShowCorrector = True + } + correctionsR whereClause colonnade filterUI csvSettings psValidator $ Map.fromList [ downloadAction , assignAction (Right shid) , autoAssignAction shid diff --git a/src/Handler/Utils/StudyFeatures.hs b/src/Handler/Utils/StudyFeatures.hs index e89b05c47..ef0d0a2e6 100644 --- a/src/Handler/Utils/StudyFeatures.hs +++ b/src/Handler/Utils/StudyFeatures.hs @@ -22,8 +22,6 @@ import Handler.Utils.StudyFeatures.Parse import qualified Data.Csv as Csv -import qualified Data.ByteString as ByteString - import qualified Data.Set as Set import Data.RFC5051 (compareUnicode) @@ -65,7 +63,7 @@ instance Csv.ToField UserTableStudyFeature where [] $ ShortStudyFieldType userTableFieldType instance Csv.ToField UserTableStudyFeatures where - toField = ByteString.intercalate "; " . map Csv.toField . view _UserTableStudyFeatures + toField = Csv.toField . CsvSemicolonList . view _UserTableStudyFeatures userTableStudyFeatureSort :: UserTableStudyFeature -> UserTableStudyFeature diff --git a/src/Handler/Utils/Submission.hs b/src/Handler/Utils/Submission.hs index 96a0710f9..b59d1d723 100644 --- a/src/Handler/Utils/Submission.hs +++ b/src/Handler/Utils/Submission.hs @@ -981,20 +981,6 @@ correctionInvisibleWidget tid ssh csh shn cID (Entity subId sub) = runMaybeT $ d return $ notification NotificationBroad =<< messageIconWidget Warning IconInvisible $(widgetFile "submission-correction-invisible") -data AuthorshipStatementSubmissionState - = ASMissing - | ASOldStatement - | ASExists - deriving (Eq, Read, Show, Enum, Bounded, Generic, Typeable) - deriving anyclass (Universe, Finite) - -deriving stock instance Ord AuthorshipStatementSubmissionState -- ^ Larger roughly encodes better; summaries are taken with `max` - -nullaryPathPiece ''AuthorshipStatementSubmissionState $ camelToPathPiece' 1 - -embedRenderMessage ''UniWorX ''AuthorshipStatementSubmissionState $ concat . ("SubmissionAuthorshipStatementState" :) . drop 1 . splitCamel - - getUserAuthorshipStatement :: ( MonadResource m , IsSqlBackend backend, SqlBackendCanRead backend ) diff --git a/src/Model/Types/DateTime.hs b/src/Model/Types/DateTime.hs index 76d427ed9..8f9a3bd28 100644 --- a/src/Model/Types/DateTime.hs +++ b/src/Model/Types/DateTime.hs @@ -133,6 +133,8 @@ instance ToJSON TermIdentifier where instance FromJSON TermIdentifier where parseJSON = withText "Term" $ either (fail . Text.unpack) return . termFromText +pathPieceCsv ''TermIdentifier + {- Must be defined in a later module: termField :: Field (HandlerT UniWorX IO) TermIdentifier termField = checkMMap (return . termFromText) termToText textField diff --git a/src/Model/Types/Submission.hs b/src/Model/Types/Submission.hs index 49dfd12ce..50bee48b8 100644 --- a/src/Model/Types/Submission.hs +++ b/src/Model/Types/Submission.hs @@ -130,3 +130,26 @@ pseudonymWords = folding pseudonymFragments :: Fold Text [PseudonymWord] pseudonymFragments = folding $ mapM (toListOf pseudonymWords) . (\l -> guard (length l == 2) *> l) . filter (not . null) . Text.split (\(CI.mk -> c) -> not $ Set.member c pseudonymCharacters) + + +instance PathPiece Pseudonym where + toPathPiece = review _PseudonymText + fromPathPiece t + | Just p <- t ^? _PseudonymText = Just p + | Just n <- fromPathPiece t = Just $ Pseudonym n + | otherwise = Nothing + +pathPieceCsv ''Pseudonym + + +data AuthorshipStatementSubmissionState + = ASMissing + | ASOldStatement + | ASExists + deriving (Eq, Read, Show, Enum, Bounded, Generic, Typeable) + deriving anyclass (Universe, Finite) + +deriving stock instance Ord AuthorshipStatementSubmissionState -- ^ Larger roughly encodes better; summaries are taken with `max` + +nullaryPathPiece ''AuthorshipStatementSubmissionState $ camelToPathPiece' 1 +pathPieceCsv ''AuthorshipStatementSubmissionState diff --git a/src/Utils/Csv.hs b/src/Utils/Csv.hs index 7070720b1..0a1d1d34d 100644 --- a/src/Utils/Csv.hs +++ b/src/Utils/Csv.hs @@ -10,6 +10,7 @@ module Utils.Csv , toCsvRendered , toDefaultOrderedCsvRendered , csvRenderedToXlsx, Xlsx, Xlsx.fromXlsx + , CsvSemicolonList(..) ) where import ClassyPrelude hiding (lookup) @@ -39,6 +40,19 @@ import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.CaseInsensitive as CI +import qualified Data.Binary.Builder as Builder +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Attoparsec.ByteString as Attoparsec + +import qualified Data.Csv.Parser as Csv +import qualified Data.Csv.Builder as Csv + +import qualified Data.Vector as Vector + +import Data.Char (ord) + +import Control.Monad.Fail + deriving instance Typeable CsvParseError instance Exception CsvParseError @@ -114,3 +128,25 @@ csvRenderedToXlsx sheetName CsvRendered{..} = def & Xlsx.atSheet sheetName ?~ (d addValues = flip foldMap (zip [2..] csvRenderedData) $ \(r, nr) -> flip foldMap (zip [1..] $ toList csvRenderedHeader) $ \(c, hBS) -> case HashMap.lookup hBS nr of Nothing -> mempty Just vBS -> Endo $ Xlsx.cellValueAtRC (r, c) ?~ Xlsx.CellText (decodeUtf8 vBS) + + +newtype CsvSemicolonList a = CsvSemicolonList { unCsvSemicolonList :: [a] } + deriving stock (Read, Show, Generic, Typeable) + deriving newtype (Eq, Ord) + +instance ToField a => ToField (CsvSemicolonList a) where + toField (CsvSemicolonList xs) = dropEnd 2 . LBS.toStrict . Builder.toLazyByteString $ Csv.encodeRecordWith encOpts fs + where + fs = map toField xs + encOpts = defaultEncodeOptions + { encDelimiter = fromIntegral $ ord ';' + , encQuoting = bool QuoteMinimal QuoteAll $ all null fs + , encUseCrLf = True + } + +instance FromField a => FromField (CsvSemicolonList a) where + parseField f + | null f = pure $ CsvSemicolonList [] + | otherwise = fmap CsvSemicolonList . mapM parseField . Vector.toList <=< either fail return $ Attoparsec.parseOnly (Csv.record sep) f + where + sep = fromIntegral $ ord ';' diff --git a/templates/i18n/changelog/corrections-csv-export.de-de-formal.hamlet b/templates/i18n/changelog/corrections-csv-export.de-de-formal.hamlet new file mode 100644 index 000000000..8a44b1939 --- /dev/null +++ b/templates/i18n/changelog/corrections-csv-export.de-de-formal.hamlet @@ -0,0 +1,2 @@ +$newline never +Tabellen von Übungsblattabgaben können nun als CSV exportiert werden diff --git a/templates/i18n/changelog/corrections-csv-export.en-eu.hamlet b/templates/i18n/changelog/corrections-csv-export.en-eu.hamlet new file mode 100644 index 000000000..70a14aa63 --- /dev/null +++ b/templates/i18n/changelog/corrections-csv-export.en-eu.hamlet @@ -0,0 +1,2 @@ +$newline never +Tables of exercise sheet submissions can now be exported as CSV diff --git a/test/Data/Scientific/InstancesSpec.hs b/test/Data/Scientific/InstancesSpec.hs new file mode 100644 index 000000000..0fb95c4f3 --- /dev/null +++ b/test/Data/Scientific/InstancesSpec.hs @@ -0,0 +1,10 @@ +module Data.Scientific.InstancesSpec where + +import TestImport +import Data.Scientific + + +spec :: Spec +spec = modifyMaxSuccess (* 10) $ + lawsCheckHspec (Proxy @Scientific) + [ pathPieceLaws ] diff --git a/test/Utils/CsvSpec.hs b/test/Utils/CsvSpec.hs new file mode 100644 index 000000000..ce556647a --- /dev/null +++ b/test/Utils/CsvSpec.hs @@ -0,0 +1,38 @@ +module Utils.CsvSpec where + +import TestImport + +import Utils.Csv + +import Data.Csv (toField, runParser, parseField) + +import Data.Char (ord) +import qualified Data.ByteString as BS + + +deriving newtype instance Arbitrary a => Arbitrary (CsvSemicolonList a) + + +spec :: Spec +spec = modifyMaxSuccess (* 10) . parallel $ do + lawsCheckHspec (Proxy @(CsvSemicolonList ByteString)) + [ csvFieldLaws ] + describe "CsvSemicolonList" $ do + let + test :: [ByteString] -> ByteString -> Expectation + test (CsvSemicolonList -> x) t = do + toField x `shouldBe` t + runParser (parseField t) `shouldBe` Right x + it "is transparent" . property $ \(bs :: ByteString) + -> let expectTransparent = BS.all (`notElem` [34, 10, 13, fromIntegral $ ord ';']) bs + && not (BS.null bs) + in expectTransparent ==> test [bs] bs + it "behaves as expected on some examples" $ do + test ["foo"] "foo" + test ["foo", "bar"] "foo;bar" + test [] "" + test [""] "\"\"" + test ["", ""] "\"\";\"\"" + test ["foo", ""] "foo;" + test ["", "foo"] ";foo" + test ["", "", "foo", ""] ";;foo;" From fe8e4bbd4f6a8b1b1c54808ebc96ee675a078648 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Wed, 18 Aug 2021 19:00:12 +0200 Subject: [PATCH 017/143] feat(corrections-r): json export --- src/Handler/Submission/List.hs | 75 +++++++++++++++++++++++++++++++++- src/Model/Types/Submission.hs | 1 + 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/Handler/Submission/List.hs b/src/Handler/Submission/List.hs index f1ded3f63..be7f33c88 100644 --- a/src/Handler/Submission/List.hs +++ b/src/Handler/Submission/List.hs @@ -302,6 +302,40 @@ data CorrectionTableCsvSettings = forall filename sheetName. , cTableShowCorrector :: Bool } +data CorrectionTableJson = CorrectionTableJson + { jsonCorrectionTerm :: TermIdentifier + , jsonCorrectionSchool :: SchoolShorthand + , jsonCorrectionCourse :: CourseShorthand + , jsonCorrectionSheet :: SheetName + , jsonCorrectionLastEdit :: Maybe UTCTime + , jsonCorrectionSubmittors :: Maybe [CorrectionTableSubmittorJson] + , jsonCorrectionAssigned :: Maybe UTCTime + , jsonCorrectionCorrectorName :: Maybe UserDisplayName + , jsonCorrectionCorrectorEmail :: Maybe UserEmail + , jsonCorrectionRatingDone :: Bool + , jsonCorrectionRatedAt :: Maybe UTCTime + , jsonCorrectionRatingPoints :: Maybe Points + , jsonCorrectionRatingComment :: Maybe Text + } deriving (Generic) + +data CorrectionTableSubmittorJson = CorrectionTableSubmittorJson + { jsonCorrectionSurname :: UserSurname + , jsonCorrectionFirstName :: UserFirstName + , jsonCorrectionName :: UserDisplayName + , jsonCorrectionMatriculation :: Maybe UserMatriculation + , jsonCorrectionEmail :: UserEmail + , jsonCorrectionPseudonym :: Maybe Pseudonym + , jsonCorrectionSubmissionGroup :: Maybe SubmissionGroupName + , jsonCorrectionAuthorshipStatementState :: Maybe AuthorshipStatementSubmissionState + } deriving (Generic) + +deriveToJSON defaultOptions + { fieldLabelModifier = camelToPathPiece' 2 + } ''CorrectionTableSubmittorJson + +deriveToJSON defaultOptions + { fieldLabelModifier = camelToPathPiece' 2 + } ''CorrectionTableJson -- Where Clauses ratedBy :: UserId -> CorrectionTableWhere @@ -667,7 +701,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' mCSVSettings psValida { dbtCsvExportForm = pure () , dbtCsvNoExportData = Just id , dbtCsvDoEncode = \() -> awaitForever $ \(_, row) -> runReaderC row $ do - submittors <- asks $ sortOn (view $ resultUserUser . $(multifocusG 2) _userSurname _userDisplayName ) . toListOf resultSubmittors + submittors <- asks $ sortOn (view $ resultUserUser . $(multifocusG 2) _userSurname _userDisplayName) . toListOf resultSubmittors forM_ (bool pure (map pure) False submittors) $ \submittors' -> transPipe (withReaderT (, submittors')) $ do let guardNonAnonymous = runMaybeT . guardMOnM (view $ _1 . resultNonAnonymousAccess) . MaybeT yieldM $ CorrectionTableCsv @@ -697,7 +731,44 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' mCSVSettings psValida , dbtCsvExampleData = Nothing } dbtCsvDecode = Nothing - dbtExtraReps = [] + dbtExtraReps = + [ DBTExtraRep $ toPrettyJSON <$> repCorrectionJson, DBTExtraRep $ toYAML <$> repCorrectionJson + ] + + repCorrectionJson :: ConduitT (E.Value SubmissionId, CorrectionTableData) Void DB (Map CryptoFileNameSubmission CorrectionTableJson) + repCorrectionJson = C.foldMap $ \(_, res) -> Map.singleton (res ^. resultCryptoID) $ mkCorrectionTableJson res + where + mkCorrectionTableJson :: CorrectionTableData -> CorrectionTableJson + mkCorrectionTableJson res' = flip runReader res' $ do + let guardNonAnonymous :: Reader CorrectionTableData (Maybe a) -> Reader CorrectionTableData (Maybe a) + guardNonAnonymous = runMaybeT . guardMOnM (view resultNonAnonymousAccess) . MaybeT + mkCorrectionTableSubmittorJson :: Reader CorrectionTableData (Maybe [CorrectionTableSubmittorJson]) + mkCorrectionTableSubmittorJson = Just <$> do + submittors <- asks $ sortOn (view $ resultUserUser . $(multifocusG 2) _userSurname _userDisplayName) . toListOf resultSubmittors + forM submittors $ \submittor -> lift . flip runReaderT submittor $ + CorrectionTableSubmittorJson + <$> view (resultUserUser . _userSurname) + <*> view (resultUserUser . _userFirstName) + <*> view (resultUserUser . _userDisplayName) + <*> view (resultUserUser . _userMatrikelnummer) + <*> view (resultUserUser . _userEmail) + <*> preview resultUserPseudonym + <*> preview resultUserSubmissionGroup + <*> preview resultUserAuthorshipStatementState + CorrectionTableJson + <$> view (resultCourseTerm . _TermId) + <*> view (resultCourseSchool . _SchoolId) + <*> view resultCourseShorthand + <*> view (resultSheet . _entityVal . _sheetName) + <*> guardNonAnonymous (preview resultLastEdit) + <*> guardNonAnonymous mkCorrectionTableSubmittorJson + <*> preview (resultSubmission . _entityVal . _submissionRatingAssigned . _Just) + <*> preview (resultCorrector . _entityVal . _userDisplayName) + <*> preview (resultCorrector . _entityVal . _userEmail) + <*> view (resultSubmission . _entityVal . to submissionRatingDone) + <*> preview (resultSubmission . _entityVal . _submissionRatingTime . _Just) + <*> preview (resultSubmission . _entityVal . _submissionRatingPoints . _Just) + <*> preview (resultSubmission . _entityVal . _submissionRatingComment . _Just) in dbTable psValidator DBTable{..} data ActionCorrections = CorrDownload diff --git a/src/Model/Types/Submission.hs b/src/Model/Types/Submission.hs index 50bee48b8..676b64776 100644 --- a/src/Model/Types/Submission.hs +++ b/src/Model/Types/Submission.hs @@ -153,3 +153,4 @@ deriving stock instance Ord AuthorshipStatementSubmissionState -- ^ Larger rough nullaryPathPiece ''AuthorshipStatementSubmissionState $ camelToPathPiece' 1 pathPieceCsv ''AuthorshipStatementSubmissionState +pathPieceJSON ''AuthorshipStatementSubmissionState From 42f1eabb2c984a7d30ea8b90710c68aff8af9f97 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Wed, 18 Aug 2021 19:00:53 +0200 Subject: [PATCH 018/143] fix(csv): less quoting in semicolon separated lists --- src/Utils/Csv.hs | 4 +++- test/Utils/CsvSpec.hs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Utils/Csv.hs b/src/Utils/Csv.hs index 0a1d1d34d..850ef9af1 100644 --- a/src/Utils/Csv.hs +++ b/src/Utils/Csv.hs @@ -140,7 +140,9 @@ instance ToField a => ToField (CsvSemicolonList a) where fs = map toField xs encOpts = defaultEncodeOptions { encDelimiter = fromIntegral $ ord ';' - , encQuoting = bool QuoteMinimal QuoteAll $ all null fs + , encQuoting = case fs of + [fStr] | null fStr -> QuoteAll + _other -> QuoteMinimal , encUseCrLf = True } diff --git a/test/Utils/CsvSpec.hs b/test/Utils/CsvSpec.hs index ce556647a..b4f1c16c0 100644 --- a/test/Utils/CsvSpec.hs +++ b/test/Utils/CsvSpec.hs @@ -32,7 +32,7 @@ spec = modifyMaxSuccess (* 10) . parallel $ do test ["foo", "bar"] "foo;bar" test [] "" test [""] "\"\"" - test ["", ""] "\"\";\"\"" + test ["", ""] ";" test ["foo", ""] "foo;" test ["", "foo"] ";foo" test ["", "", "foo", ""] ";;foo;" From 7aadb6662bc8db76436f8d41ded7156acb98418e Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Wed, 18 Aug 2021 20:59:52 +0200 Subject: [PATCH 019/143] feat(corrections-r): allow csv exporting one line per submittor --- .../courses/submission/de-de-formal.msg | 2 ++ .../categories/courses/submission/en-eu.msg | 2 ++ src/Data/Scientific/Instances.hs | 10 +++++++++- src/Handler/Submission/List.hs | 17 ++++++++++++----- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/messages/uniworx/categories/courses/submission/de-de-formal.msg b/messages/uniworx/categories/courses/submission/de-de-formal.msg index b2b734946..145768cc4 100644 --- a/messages/uniworx/categories/courses/submission/de-de-formal.msg +++ b/messages/uniworx/categories/courses/submission/de-de-formal.msg @@ -251,6 +251,8 @@ CsvColumnCorrectionAssigned: Zeitpunkt der Zuteilung des Korrektors (ISO 8601) CsvColumnCorrectionLastEdit: Zeitpunkt der letzten Änderung der Abgabe (ISO 8601) CsvColumnCorrectionRatingPoints: Erreichte Punktezahl (Für “_{MsgSheetGradingPassBinary}” entspricht 0 “_{MsgRatingNotPassed}” und alles andere “_{MsgRatingPassed}”) CsvColumnCorrectionRatingComment: Bewertungskommentar +CorrectionCsvSingleSubmittors: Eine Zeile pro Abgebende:n +CorrectionCsvSingleSubmittorsTip: Sollen Abgaben mit mehreren Abgebenden mehrfach vorkommen, sodass jeweils eine Zeile pro Abgebende:n enthalten ist, statt mehrere Abgebende in einer Zeile zusammenzufassen? CorrectionTableCsvNameSheetCorrections tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}-abgaben CorrectionTableCsvSheetNameSheetCorrections tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn} Abgaben diff --git a/messages/uniworx/categories/courses/submission/en-eu.msg b/messages/uniworx/categories/courses/submission/en-eu.msg index 1e9adbd3b..0574c4a9d 100644 --- a/messages/uniworx/categories/courses/submission/en-eu.msg +++ b/messages/uniworx/categories/courses/submission/en-eu.msg @@ -250,6 +250,8 @@ CsvColumnCorrectionAssigned: Timestamp of when corrector was assigned (ISO 8601) CsvColumnCorrectionLastEdit: Timestamp of the last edit of the submission (ISO 8601) CsvColumnCorrectionRatingPoints: Achieved points (for “_{MsgSheetGradingPassBinary}” 0 means “_{MsgRatingNotPassed}”, everything else means “_{MsgRatingPassed}”) CsvColumnCorrectionRatingComment: Rating comment +CorrectionCsvSingleSubmittors: One row per submittor +CorrectionCsvSingleSubmittorsTip: Should submissions with multiple submittors be split into multiple rows, such that there is one row per submittor instead of having multiple submittors within one row? CorrectionTableCsvNameSheetCorrections tid ssh csh shn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}-submissions CorrectionTableCsvSheetNameSheetCorrections tid ssh csh shn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn} Submissions diff --git a/src/Data/Scientific/Instances.hs b/src/Data/Scientific/Instances.hs index 8c0c83e89..87b079e7e 100644 --- a/src/Data/Scientific/Instances.hs +++ b/src/Data/Scientific/Instances.hs @@ -11,7 +11,15 @@ import Web.PathPieces import Text.ParserCombinators.ReadP (readP_to_S) +import Control.Monad.Fail + instance PathPiece Scientific where toPathPiece = pack . formatScientific Fixed Nothing - fromPathPiece = fmap fst . listToMaybe . filter (\(_, rStr) -> null rStr) . readP_to_S scientificP . unpack + + fromPathPiece = disambiguate . readP_to_S scientificP . unpack + where + disambiguate strs = case filter (\(_, rStr) -> null rStr) strs of + [(x, _)] -> pure x + _other -> fail "fromPathPiece Scientific: Ambiguous parse" + diff --git a/src/Handler/Submission/List.hs b/src/Handler/Submission/List.hs index be7f33c88..d9976e95c 100644 --- a/src/Handler/Submission/List.hs +++ b/src/Handler/Submission/List.hs @@ -302,6 +302,12 @@ data CorrectionTableCsvSettings = forall filename sheetName. , cTableShowCorrector :: Bool } +newtype CorrectionTableCsvExportData = CorrectionTableCsvExportData + { csvCorrectionSingleSubmittors :: Bool + } deriving (Eq, Ord, Read, Show, Generic, Typeable) +instance Default CorrectionTableCsvExportData where + def = CorrectionTableCsvExportData False + data CorrectionTableJson = CorrectionTableJson { jsonCorrectionTerm :: TermIdentifier , jsonCorrectionSchool :: SchoolShorthand @@ -698,11 +704,12 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' mCSVSettings psValida dbtCsvEncode = do CorrectionTableCsvSettings{..} <- mCSVSettings return DBTCsvEncode - { dbtCsvExportForm = pure () - , dbtCsvNoExportData = Just id - , dbtCsvDoEncode = \() -> awaitForever $ \(_, row) -> runReaderC row $ do + { dbtCsvExportForm = CorrectionTableCsvExportData + <$> apopt checkBoxField (fslI MsgCorrectionCsvSingleSubmittors & setTooltip MsgCorrectionCsvSingleSubmittorsTip) (Just $ csvCorrectionSingleSubmittors def) + , dbtCsvNoExportData = Nothing + , dbtCsvDoEncode = \CorrectionTableCsvExportData{..} -> awaitForever $ \(_, row) -> runReaderC row $ do submittors <- asks $ sortOn (view $ resultUserUser . $(multifocusG 2) _userSurname _userDisplayName) . toListOf resultSubmittors - forM_ (bool pure (map pure) False submittors) $ \submittors' -> transPipe (withReaderT (, submittors')) $ do + forM_ (bool pure (map pure) csvCorrectionSingleSubmittors submittors) $ \submittors' -> transPipe (withReaderT (, submittors')) $ do let guardNonAnonymous = runMaybeT . guardMOnM (view $ _1 . resultNonAnonymousAccess) . MaybeT yieldM $ CorrectionTableCsv <$> preview (_1 . resultCourseTerm . _TermId) @@ -731,7 +738,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' mCSVSettings psValida , dbtCsvExampleData = Nothing } dbtCsvDecode = Nothing - dbtExtraReps = + dbtExtraReps = maybe id (\CorrectionTableCsvSettings{..} -> withCsvExtraRep cTableCsvSheetName (def :: CorrectionTableCsvExportData) dbtCsvEncode) mCSVSettings [ DBTExtraRep $ toPrettyJSON <$> repCorrectionJson, DBTExtraRep $ toYAML <$> repCorrectionJson ] From 482241d033c32c52c31ea20920a4fec07ba975dd Mon Sep 17 00:00:00 2001 From: ros Date: Fri, 2 Jul 2021 14:32:13 +0200 Subject: [PATCH 020/143] feat(sorting tutorial table): done --- src/Application.hs | 2 +- src/Handler/Tutorial/List.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Application.hs b/src/Application.hs index 7d02e6009..54213df10 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -718,4 +718,4 @@ addPWEntry :: User addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do PWHashConf{..} <- getsYesod $ view _appAuthPWHash (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength - void $ insert User{..} + void $ insert User{..} \ No newline at end of file diff --git a/src/Handler/Tutorial/List.hs b/src/Handler/Tutorial/List.hs index 39f67c0e8..b49eea883 100644 --- a/src/Handler/Tutorial/List.hs +++ b/src/Handler/Tutorial/List.hs @@ -42,7 +42,7 @@ getCTutorialListR tid ssh csh = do dbtColonnade = dbColonnade $ mconcat [ sortable (Just "type") (i18nCell MsgTableTutorialType) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) -> textCell $ CI.original tutorialType , sortable (Just "name") (i18nCell MsgTableTutorialName) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) -> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) [whamlet|#{tutorialName}|] - , sortable Nothing (i18nCell MsgTableTutorialTutors) $ \(view $ resultTutorial . _entityKey -> tutid) -> sqlCell $ do + , sortable (Just "tutors") (i18nCell MsgTableTutorialTutors) $ \(view $ resultTutorial . _entityKey -> tutid) -> sqlCell $ do tutors <- fmap (map $(unValueN 3)) . E.select . E.from $ \(tutor `E.InnerJoin` user) -> do E.on $ tutor E.^. TutorUser E.==. user E.^. UserId E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid From b1787cd77e8a643accc0ef54cc18c87df215680c Mon Sep 17 00:00:00 2001 From: ros Date: Thu, 8 Jul 2021 11:14:22 +0200 Subject: [PATCH 021/143] feat(tutor tabel sorting): dbt sorting tutors added --- src/Handler/Tutorial/List.hs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Handler/Tutorial/List.hs b/src/Handler/Tutorial/List.hs index b49eea883..9043cb08d 100644 --- a/src/Handler/Tutorial/List.hs +++ b/src/Handler/Tutorial/List.hs @@ -71,6 +71,12 @@ getCTutorialListR tid ssh csh = do dbtSorting = Map.fromList [ ("type", SortColumn $ \tutorial -> tutorial E.^. TutorialType ) , ("name", SortColumn $ \tutorial -> tutorial E.^. TutorialName ) + , ( "tutors" + , SortColumn $ \tutorial -> E.subSelectMaybe . E.from $ \(tutor `E.InnerJoin` user) -> do + E.on $ tutor E.^. TutorUser E.==. user E.^. UserId + E.where_ $ tutorial E.^. TutorialId E.==. tutor E.^. TutorTutorial + return . E.min_ $ user E.^. UserSurname + ) , ("participants", SortColumn $ \tutorial -> let participantCount :: E.SqlExpr (E.Value Int) participantCount = E.subSelectCount . E.from $ \tutorialParticipant -> E.where_ $ tutorialParticipant E.^. TutorialParticipantTutorial E.==. tutorial E.^. TutorialId From 9dc12de056e73736659c053b0eabef66ca524047 Mon Sep 17 00:00:00 2001 From: ros Date: Tue, 17 Aug 2021 11:46:54 +0200 Subject: [PATCH 022/143] feat(sorting tutorial table): application restore --- src/Application.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.hs b/src/Application.hs index 54213df10..7d02e6009 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -718,4 +718,4 @@ addPWEntry :: User addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do PWHashConf{..} <- getsYesod $ view _appAuthPWHash (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength - void $ insert User{..} \ No newline at end of file + void $ insert User{..} From acab92d143efb950d2449309a622560b41a7b2e9 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 20 Aug 2021 12:19:29 +0200 Subject: [PATCH 023/143] chore: restore testdata/workflows --- testdata/workflows | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/workflows b/testdata/workflows index cf7dcf58c..1a788c67f 160000 --- a/testdata/workflows +++ b/testdata/workflows @@ -1 +1 @@ -Subproject commit cf7dcf58c524176bbdd27ff279d68a5ab90cd06e +Subproject commit 1a788c67fe98cadf1e29b0e328072437955fd660 From c3b453c147fb0d1cfe247c547318c4d683dd1af8 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 20 Aug 2021 16:09:49 +0200 Subject: [PATCH 024/143] chore(release): 25.21.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- package.yaml | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e679e1592..22a504a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.21.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.20.2...v25.21.0) (2021-08-20) + + +### Features + +* **corrections-r:** allow csv exporting one line per submittor ([7aadb66](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7aadb6662bc8db76436f8d41ded7156acb98418e)) +* **corrections-r:** authorship statement state ([51522ef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/51522efc7c9915115e0d8791320a03e35d2933c8)) +* **corrections-r:** csv export ([2a6248e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2a6248e3d5d4f4de5f1c7d6c6bcf092dc9873a2e)), closes [#705](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/705) +* **corrections-r:** filter/sort by pseudonym ([153af8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/153af8c6b4042430bb4bc120fa5c24a5d114e4c1)) +* **corrections-r:** json export ([fe8e4bb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fe8e4bbd4f6a8b1b1c54808ebc96ee675a078648)) +* **course admin:** application restore ([cb4ed8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cb4ed8d9887e521f47689c118baf439846cd4514)) +* **course admin:** done ([15689c5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/15689c597ef407583b01dabc9f7631e9dc90b009)) +* **course admin:** no new-line ([0a6a174](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0a6a1749d351e626383e513293af280f78552009)) +* **lecturer type:** aenderung ([89e1d67](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89e1d675c3be0fec106e84920184a8c95dfa6346)) +* **link password time:** application restore ([6d536c3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6d536c39bd9f3117f18d2e52c93f178aea4a002d)) +* **link password time:** done ([4490e9a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4490e9ad20c55153e81a344c7dbf7813cb219108)) +* **link password time:** done ([2321216](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2321216b0f4f194c7cd8b47eb020819d6aa1f2e5)) +* **link password time:** new time format ([df2a9bc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/df2a9bc20fe9f958cbee98315b644ec2fcba0630)) +* **link password time:** restore application ([c5c5417](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c5c541709b5053c08d21bdd753bb99df574c6c5b)) +* **link password time:** restore application ([85006ff](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/85006ff389188b56a8b61943621c190c9a9503b7)) +* **sorting tutorial table:** application restore ([9dc12de](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9dc12de056e73736659c053b0eabef66ca524047)) +* **sorting tutorial table:** done ([482241d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/482241d033c32c52c31ea20920a4fec07ba975dd)) +* **tutor tabel sorting:** dbt sorting tutors added ([b1787cd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b1787cd77e8a643accc0ef54cc18c87df215680c)) + + +### Bug Fixes + +* **corrections-r:** allow filtering by matriculation ([1b6b781](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1b6b781e82c39bc29c8984c587ac836f0da77a02)) +* **csv:** less quoting in semicolon separated lists ([42f1eab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42f1eabb2c984a7d30ea8b90710c68aff8af9f97)) + ## [25.20.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.20.1...v25.20.2) (2021-08-16) diff --git a/package-lock.json b/package-lock.json index f69da8c57..38b149cfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.20.2", + "version": "25.21.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 180c83d28..727983d52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.20.2", + "version": "25.21.0", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index a2afdc4f7..2793c89b4 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.20.2 +version: 25.21.0 dependencies: - base - yesod From 7d7c37662211881c69d6543f295ec4d7360abd22 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Wed, 25 Aug 2021 11:53:08 +0200 Subject: [PATCH 025/143] chore(gitlab-ci): bump docker image --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 176f7d6ed..7ad39f07e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,7 @@ workflow: default: image: - name: fpco/stack-build:lts-17.15 + name: fpco/stack-build:lts-18.0 variables: STACK_ROOT: "${CI_PROJECT_DIR}/.stack" From c1c35369d1ae017971e6d8cbafc06844f02fd00d Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 14 Jun 2021 14:44:03 +0200 Subject: [PATCH 026/143] feat: implemented an event manager --- .../src/lib/event-manager/event-manager.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 frontend/src/lib/event-manager/event-manager.js diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js new file mode 100644 index 000000000..810cfab55 --- /dev/null +++ b/frontend/src/lib/event-manager/event-manager.js @@ -0,0 +1,65 @@ + +export const EVENT_TYPE = { + CLICK : 'click', + KEYDOWN : 'keydown', + //more to be added +}; + + + +export class EventManager { + _registeredListeners; + + + constructor() { + this._registeredListeners = []; + } + + registerNewListener(eventWrapper) { + this._debugLog('registerNewListener', eventWrapper); + let element = eventWrapper.element; + element.addEventListener(eventWrapper.eventType, eventWrapper.eventHandler); + this._registeredListeners.push(eventWrapper); + } + + removeAllEventListenersFromUtil() { + this._debugLog('removeAllEventListenersFromUtil',); + for (let eventWrapper of this._registeredListeners) { + let element = eventWrapper.element; + element.removeEventListener(eventWrapper.eventType, eventWrapper.eventHandler); + } + this._registeredListeners = []; + } + + //_debugLog() {} + _debugLog(fName, ...args) { + console.log(`[DEBUGLOG] EventManager.${fName}`, { args: args, instance: this }); + } +} + +export class EventWrapper { + _eventType; + _eventHandler; + _element; + + constructor(_eventType, _eventHandler, _element) { + if(!_eventType || !_eventHandler || !_element) { + throw new Error('Not enough arguments!'); + } + this._eventType = _eventType; + this._eventHandler = _eventHandler; + this._element = _element; + } + + get eventType() { + return this._eventType; + } + + get eventHandler() { + return this._eventHandler; + } + + get element() { + return this._element; + } +} \ No newline at end of file From d1b995269060c670d85f715671bfc9947b9f3e9a Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 14 Jun 2021 14:53:07 +0200 Subject: [PATCH 027/143] fix(enter-is-tab.js): implemented destroy method in enter-is-tab Util --- frontend/src/utils/form/enter-is-tab.js | 44 +++++++++++++++---------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/frontend/src/utils/form/enter-is-tab.js b/frontend/src/utils/form/enter-is-tab.js index 4e171c87b..0a3073b9e 100644 --- a/frontend/src/utils/form/enter-is-tab.js +++ b/frontend/src/utils/form/enter-is-tab.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const ENTER_IS_TAB_INITIALIZED_CLASS = 'enter-as-tab--initialized'; const AREA_SELECTOR = 'input, textarea'; @@ -10,6 +11,8 @@ const AREA_SELECTOR = 'input, textarea'; export class EnterIsTab { _element; + _eventManager; + constructor(element) { if(!element) { @@ -17,6 +20,8 @@ export class EnterIsTab { } this._element = element; + + this._eventManager = new EventManager(); if (this._element.classList.contains(ENTER_IS_TAB_INITIALIZED_CLASS)) { return false; @@ -27,27 +32,32 @@ export class EnterIsTab { start() { - this._element.addEventListener('keydown', (e) => { - if(e.key === 'Enter') { - e.preventDefault(); - let currentInputFieldId = this._element.id; - let inputAreas = document.querySelectorAll(AREA_SELECTOR); - let nextInputArea = null; - for (let i = 0; i < inputAreas.length; i++) { - if(inputAreas[i].id === currentInputFieldId) { - nextInputArea = inputAreas[i+1]; - break; - } - } - - if(nextInputArea) { - nextInputArea.focus(); + let eventWrapper = new EventWrapper(EVENT_TYPE.KEYDOWN, this._captureEnter.bind(this), this._element); + this._eventManager.registerNewListener(eventWrapper); + } + + _captureEnter (e) { + if(e.key === 'Enter') { + e.preventDefault(); + let currentInputFieldId = this._element.id; + let inputAreas = document.querySelectorAll(AREA_SELECTOR); + let nextInputArea = null; + for (let i = 0; i < inputAreas.length; i++) { + if(inputAreas[i].id === currentInputFieldId) { + nextInputArea = inputAreas[i+1]; + break; } } - }); + + if(nextInputArea) { + nextInputArea.focus(); + } + } } destroy() { - console.log('TBD: Destroy EnterIsTab'); + this._eventManager.removeAllEventListenersFromUtil(); + if(this._element.classList.contains(ENTER_IS_TAB_INITIALIZED_CLASS)) + this._element.classList.remove(ENTER_IS_TAB_INITIALIZED_CLASS); } } \ No newline at end of file From f1ef2e5ec776ad8a1fb7737eb0ed4f79218afd61 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 18 Jun 2021 13:51:02 +0200 Subject: [PATCH 028/143] feat(util_registry): impelmented destroyAll(scope) method in the utilRegistry --- .../src/lib/event-manager/event-manager.js | 2 + .../services/util-registry/util-registry.js | 45 ++++++++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index 810cfab55..e8121131f 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -31,6 +31,8 @@ export class EventManager { this._registeredListeners = []; } + + //Todo: Uncomment debug log! //_debugLog() {} _debugLog(fName, ...args) { console.log(`[DEBUGLOG] EventManager.${fName}`, { args: args, instance: this }); diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index 65e9326cc..dd4a38af6 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -4,8 +4,8 @@ const DEBUG_MODE = /localhost/.test(window.location.href) ? 1 : 0; export class UtilRegistry { - _registeredUtils = new Array(); - _activeUtilInstances = new Array(); + _registeredUtilClasses = new Array(); //{utilClass} + _activeUtilInstancesWrapped = new Array(); //{utilClass, scope, element, instance} _appInstance; /** @@ -33,7 +33,7 @@ export class UtilRegistry { console.log('registering util "' + util.name + '"'); console.log({ util }); } - this._registeredUtils.push(util); + this._registeredUtilClasses.push(util); } deregister(name, destroy) { @@ -44,7 +44,7 @@ export class UtilRegistry { this._destroyUtilInstances(name); } - this._registeredUtils.splice(utilIndex, 1); + this._registeredUtilClasses.splice(utilIndex, 1); } } @@ -54,7 +54,7 @@ export class UtilRegistry { initAll(scope = document.body) { let startedInstances = new Array(); - const setupInstances = this._registeredUtils.map((util) => this.setup(util, scope)).flat(); + const setupInstances = this._registeredUtilClasses.map((util) => this.setup(util, scope)).flat(); const orderedInstances = setupInstances.filter(_isStartOrdered); @@ -97,6 +97,17 @@ export class UtilRegistry { return startedInstances; } + destroyAll(scope = document.body) { + let utilsInScope = this._getUtilInstancesWithinScope(scope); + + utilsInScope.forEach((util) => { + //if(DEBUG_MODE > 2) { + console.log('Destroying Util: ', {util}); + //} + util.destroy(); + }); + } + setup(util, scope = document.body) { if (DEBUG_MODE > 2) { console.log('setting up util', { util }); @@ -130,12 +141,12 @@ export class UtilRegistry { }); } - this._activeUtilInstances.push(...instances); + this._activeUtilInstancesWrapped.push(...instances); return instances; } find(name) { - return this._registeredUtils.find((util) => util.name === name); + return this._registeredUtilClasses.find((util) => util.name === name); } _findUtilElements(util, scope) { @@ -146,11 +157,23 @@ export class UtilRegistry { } _findUtilIndex(name) { - return this._registeredUtils.findIndex((util) => util.name === name); + return this._registeredUtilClasses.findIndex((util) => util.name === name); + } + + _getUtilInstancesWithinScope(scope) { + let utilInstances = []; + + for (let activeUtilInstance of this._activeUtilInstancesWrapped) { + let util = activeUtilInstance.util; + if(this._findUtilElements(util, scope).length > 0) { + utilInstances.push(activeUtilInstance.instance); + } + } + return utilInstances; } _destroyUtilInstances(name) { - this._activeUtilInstances + this._activeUtilInstancesWrapped .map((util, index) => ({ util: util, index: index, @@ -159,11 +182,11 @@ export class UtilRegistry { .forEach((activeUtil) => { // destroy util instance activeUtil.util.destroy(); - delete this._activeUtilInstances[activeUtil.index]; + delete this._activeUtilInstancesWrapped[activeUtil.index]; }); // get rid of now empty array slots - this._activeUtilInstances = this._activeUtilInstances.filter((util) => !!util); + this._activeUtilInstancesWrapped = this._activeUtilInstancesWrapped.filter((util) => !!util); } } From 01c239d6c3c29b68e191c1f5e866391ab5020b07 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 21 Jun 2021 12:23:08 +0200 Subject: [PATCH 029/143] chore(util_registry_test): added test case to deregister method when instances should be destroyed --- .../services/util-registry/util-registry.js | 9 +-- .../util-registry/util-registry.spec.js | 65 ++++++++++++++----- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index dd4a38af6..b342d812a 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -178,15 +178,12 @@ export class UtilRegistry { util: util, index: index, })) - .filter((activeUtil) => activeUtil.util.name === name) + .filter((activeUtil) => activeUtil.util.util.name === name) .forEach((activeUtil) => { // destroy util instance - activeUtil.util.destroy(); - delete this._activeUtilInstancesWrapped[activeUtil.index]; + activeUtil.util.instance.destroy(); + this._activeUtilInstancesWrapped = this._activeUtilInstancesWrapped.splice(activeUtil.index, 1); }); - - // get rid of now empty array slots - this._activeUtilInstancesWrapped = this._activeUtilInstancesWrapped.filter((util) => !!util); } } diff --git a/frontend/src/services/util-registry/util-registry.spec.js b/frontend/src/services/util-registry/util-registry.spec.js index 07b9e2627..00f8806b3 100644 --- a/frontend/src/services/util-registry/util-registry.spec.js +++ b/frontend/src/services/util-registry/util-registry.spec.js @@ -24,24 +24,6 @@ describe('UtilRegistry', () => { }); }); - describe('deregister()', () => { - it('should remove util', () => { - // register util - utilRegistry.register(TestUtil1); - let foundUtil = utilRegistry.find(TestUtil1.name); - expect(foundUtil).toBeTruthy(); - - // deregister util - utilRegistry.deregister(TestUtil1.name); - foundUtil = utilRegistry.find(TestUtil1.name); - expect(foundUtil).toBeFalsy(); - }); - - it('should destroy util instances if requested', () => { - pending('TBD'); - }); - }); - describe('setup()', () => { it('should catch errors thrown by the utility', () => { @@ -107,6 +89,51 @@ describe('UtilRegistry', () => { }); }); + describe('deregister()', () => { + let testScope; + let testElement1; + let testElement2; + + beforeEach(() => { + testScope = document.createElement('div'); + testElement1 = document.createElement('div'); + testElement2 = document.createElement('div'); + testElement1.classList.add('util1'); + testElement2.classList.add('util1'); + testScope.appendChild(testElement1); + testScope.appendChild(testElement2); + }); + + it('should remove util', () => { + // register util + utilRegistry.register(TestUtil1); + let foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeTruthy(); + + // deregister util + utilRegistry.deregister(TestUtil1.name); + foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeFalsy(); + }); + + it('should destroy util instances if requested', () => { + utilRegistry.register(TestUtil1); + let foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeTruthy(); + + utilRegistry.setup(TestUtil1, testScope); + let firstActiveUtil = utilRegistry._activeUtilInstancesWrapped[0]; + expect(utilRegistry._activeUtilInstancesWrapped.length).toEqual(2); + expect(utilRegistry._activeUtilInstancesWrapped[0].element).toEqual(testElement1); + + spyOn(firstActiveUtil.instance, 'destroy'); + + utilRegistry.deregister(TestUtil1.name, true); + expect(utilRegistry._activeUtilInstancesWrapped[0]).toBeFalsy(); + expect(firstActiveUtil.instance.destroy).toHaveBeenCalled(); + }); + }); + describe('initAll()', () => { it('should setup all the utilities', () => { spyOn(utilRegistry, 'setup'); @@ -181,6 +208,8 @@ class TestUtil1 { this.element = element; this.app = app; } + + destroy() {} } @Utility({ selector: '#util2' }) From ded4c1f64e22c0d3e6cd4d10aea518d044656480 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 21 Jun 2021 16:03:36 +0200 Subject: [PATCH 030/143] chore(util-registry.spec): added a test for destroyAll Method --- .../services/util-registry/util-registry.js | 4 +- .../util-registry/util-registry.spec.js | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index b342d812a..b8dea6ec3 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -103,8 +103,10 @@ export class UtilRegistry { utilsInScope.forEach((util) => { //if(DEBUG_MODE > 2) { console.log('Destroying Util: ', {util}); - //} + //}# + let utilIndex = this._activeUtilInstancesWrapped.indexOf(util); util.destroy(); + this._activeUtilInstancesWrapped.splice(utilIndex, 1); }); } diff --git a/frontend/src/services/util-registry/util-registry.spec.js b/frontend/src/services/util-registry/util-registry.spec.js index 00f8806b3..18506be99 100644 --- a/frontend/src/services/util-registry/util-registry.spec.js +++ b/frontend/src/services/util-registry/util-registry.spec.js @@ -199,6 +199,44 @@ describe('UtilRegistry', () => { }); }); }); + + describe('destroyAll()', () => { + let testScope; + let testElement; + let firstUtil; + + beforeEach( () => { + testScope = document.createElement('div'); + testElement = document.createElement('div'); + testElement.classList.add('util3'); + testScope.appendChild(testElement); + + utilRegistry.register(TestUtil3); + utilRegistry.initAll(testScope); + + firstUtil = utilRegistry._activeUtilInstancesWrapped[0]; + spyOn(firstUtil.instance, 'destroy'); + }); + + it('Util should be destroyed', () => { + utilRegistry.destroyAll(testScope); + expect(utilRegistry._activeUtilInstancesWrapped.length).toBe(0); + expect(firstUtil.instance.destroy).toHaveBeenCalled(); + }); + + it('Util out of scope should not be destroyed', () => { + let outOfScope = document.createElement('div'); + expect(utilRegistry._activeUtilInstancesWrapped.length).toEqual(1); + + utilRegistry.destroyAll(outOfScope); + + expect(utilRegistry._activeUtilInstancesWrapped.length).toEqual(1); + expect(utilRegistry._activeUtilInstancesWrapped[0]).toBe(firstUtil); + expect(firstUtil.instance.destroy).not.toHaveBeenCalled(); + + }); + }); + }); // test utilities @@ -219,6 +257,7 @@ class TestUtil2 { } class TestUtil3 { constructor() {} start() {} + destroy() {} } @Utility({ selector: '#throws' }) From b2c3f77966dd16d282938fb907c5dcc29b1f84a3 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 25 Jun 2021 11:41:44 +0200 Subject: [PATCH 031/143] chore(form-error-reporter): implemented destroy in form-error-reporter --- .../src/lib/event-manager/event-manager.js | 2 ++ .../src/utils/form/form-error-reporter.js | 33 +++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index e8121131f..e133ce018 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -2,6 +2,8 @@ export const EVENT_TYPE = { CLICK : 'click', KEYDOWN : 'keydown', + INVALID : 'invalid', + CHANGE : 'change', //more to be added }; diff --git a/frontend/src/utils/form/form-error-reporter.js b/frontend/src/utils/form/form-error-reporter.js index dff3f00d2..8ebd14d5f 100644 --- a/frontend/src/utils/form/form-error-reporter.js +++ b/frontend/src/utils/form/form-error-reporter.js @@ -1,5 +1,6 @@ import { Utility } from '../../core/utility'; import * as defer from 'lodash.defer'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const FORM_ERROR_REPORTER_INITIALIZED_CLASS = 'form-error-remover--initialized'; @@ -10,12 +11,16 @@ export class FormErrorReporter { _element; _err; + _eventManager; + constructor(element) { if (!element) throw new Error('Form Error Reporter utility needs to be passed an element!'); this._element = element; + this._eventManager = new EventManager(); + if (this._element.classList.contains(FORM_ERROR_REPORTER_INITIALIZED_CLASS)) return; @@ -24,11 +29,23 @@ export class FormErrorReporter { start() { if (this._element.willValidate) { - this._element.addEventListener('invalid', this.report.bind(this)); - this._element.addEventListener('change', () => { defer(this.report.bind(this)); } ); + let invalidElementEvent = new EventWrapper(EVENT_TYPE.INVALID, this.report.bind(this), this._element); + this._eventManager.registerNewListener(invalidElementEvent); + + let changedElementEvent = new EventWrapper(EVENT_TYPE.CHANGE, () => { defer(this.report.bind(this)); }, this._element); + this._eventManager.registerNewListener(changedElementEvent); } } + destroy() { + this._eventManager.removeAllEventListenersFromUtil(); + + this._removeError(); + + if(this._element.classList.contains(FORM_ERROR_REPORTER_INITIALIZED_CLASS)) + this._element.classList.remove(FORM_ERROR_REPORTER_INITIALIZED_CLASS); + } + report() { const msg = this._element.validity.valid ? null : this._element.validationMessage; @@ -37,10 +54,7 @@ export class FormErrorReporter { if (!target) return; - if (this._err && this._err.parentNode) { - this._err.parentNode.removeChild(this._err); - this._err = undefined; - } + this._removeError(); if (!msg) { target.classList.remove('standalone-field--has-error', 'form-group--has-error'); @@ -65,4 +79,11 @@ export class FormErrorReporter { } } } + + _removeError() { + if (this._err && this._err.parentNode) { + this._err.parentNode.removeChild(this._err); + this._err = undefined; + } + } } From 4812db6c8756a52e7f9db26b8e620c0f38a4546f Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 25 Jun 2021 12:19:13 +0200 Subject: [PATCH 032/143] chore(alerts): implemented destroy method in alerts.js --- frontend/src/utils/alerts/alerts.js | 29 +++++++++++++++++++----- frontend/src/utils/alerts/alerts.spec.js | 8 ++++++- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/frontend/src/utils/alerts/alerts.js b/frontend/src/utils/alerts/alerts.js index dcecc915b..4f84d8aac 100644 --- a/frontend/src/utils/alerts/alerts.js +++ b/frontend/src/utils/alerts/alerts.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './alerts.sass'; const ALERTS_INITIALIZED_CLASS = 'alerts--initialized'; @@ -32,6 +33,8 @@ export class Alerts { _element; _app; + _eventManager; + constructor(element, app) { if (!element) { throw new Error('Alerts util has to be called with an element!'); @@ -40,6 +43,8 @@ export class Alerts { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (this._element.classList.contains(ALERTS_INITIALIZED_CLASS)) { return false; } @@ -48,6 +53,7 @@ export class Alerts { this._alertElements = this._gatherAlertElements(); if (this._togglerElement) { + //should there be a start method, to initialize the listeners in initToggler and initAlerts or is this wanted? this._initToggler(); } @@ -61,7 +67,14 @@ export class Alerts { } destroy() { - console.log('TBD: Destroy Alert'); + this._eventManager.removeAllEventListenersFromUtil(); + + if(this._alertElements) { + this._alertElements.forEach(element => element.remove() ); + } + + if(this._element.classList.contains(ALERTS_INITIALIZED_CLASS)) + this._element.classList.remove(ALERTS_INITIALIZED_CLASS); } _gatherAlertElements() { @@ -71,10 +84,12 @@ export class Alerts { } _initToggler() { - this._togglerElement.addEventListener('click', () => { + let clickListenerToggler = new EventWrapper(EVENT_TYPE.CLICK, () => { this._alertElements.forEach((alertEl) => this._toggleAlert(alertEl, true)); this._togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS); - }); + }, this._togglerElement); + + this._eventManager.registerNewListener(clickListenerToggler); } _initAlerts() { @@ -88,9 +103,11 @@ export class Alerts { } const closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS); - closeEl.addEventListener('click', () => { - this._toggleAlert(alertElement); - }); + const closeAlertEvent = new EventWrapper(EVENT_TYPE.CLICK, () => { + this._toggleAlert(alertElement).bind(this); + }, closeEl); + + this._eventManager.registerNewListener(closeAlertEvent); if (autoHideDelay > 0 && alertElement.matches(ALERT_AUTOCLOSING_MATCHER)) { window.setTimeout(() => this._toggleAlert(alertElement), autoHideDelay * 1000); diff --git a/frontend/src/utils/alerts/alerts.spec.js b/frontend/src/utils/alerts/alerts.spec.js index 0b4749e97..889229f7c 100644 --- a/frontend/src/utils/alerts/alerts.spec.js +++ b/frontend/src/utils/alerts/alerts.spec.js @@ -1,4 +1,4 @@ -import { Alerts } from './alerts'; +import { Alerts, ALERTS_INITIALIZED_CLASS } from './alerts'; const MOCK_APP = { httpClient: { @@ -19,6 +19,12 @@ describe('Alerts', () => { expect(alerts).toBeTruthy(); }); + it('should destory alerts', () => { + alerts.destroy(); + expect(alerts._eventManager._registeredListeners.length).toBe(0); + expect(alerts._element.classList).not.toContain(ALERTS_INITIALIZED_CLASS); + }); + it('should throw if called without an element', () => { expect(() => { new Alerts(); From a25b59b3184c8b8e77b05c7defbe7301565dd60b Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 25 Jun 2021 13:20:21 +0200 Subject: [PATCH 033/143] chore(asidenav): implemented destroy in asidenav.js --- .../src/lib/event-manager/event-manager.js | 11 ++++++++-- frontend/src/utils/asidenav/asidenav.js | 22 ++++++++++++++----- frontend/src/utils/asidenav/asidenav.spec.js | 8 ++++++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index e133ce018..a73a30fc2 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -4,6 +4,7 @@ export const EVENT_TYPE = { KEYDOWN : 'keydown', INVALID : 'invalid', CHANGE : 'change', + MOUSE_OVER : 'mouseover', //more to be added }; @@ -20,7 +21,7 @@ export class EventManager { registerNewListener(eventWrapper) { this._debugLog('registerNewListener', eventWrapper); let element = eventWrapper.element; - element.addEventListener(eventWrapper.eventType, eventWrapper.eventHandler); + element.addEventListener(eventWrapper.eventType, eventWrapper.eventHandler, eventWrapper.options); this._registeredListeners.push(eventWrapper); } @@ -45,14 +46,16 @@ export class EventWrapper { _eventType; _eventHandler; _element; + _options - constructor(_eventType, _eventHandler, _element) { + constructor(_eventType, _eventHandler, _element, _options) { if(!_eventType || !_eventHandler || !_element) { throw new Error('Not enough arguments!'); } this._eventType = _eventType; this._eventHandler = _eventHandler; this._element = _element; + this._options = _options; } get eventType() { @@ -66,4 +69,8 @@ export class EventWrapper { get element() { return this._element; } + + get options() { + return this._options; + } } \ No newline at end of file diff --git a/frontend/src/utils/asidenav/asidenav.js b/frontend/src/utils/asidenav/asidenav.js index 6bf425ffa..560183e8c 100644 --- a/frontend/src/utils/asidenav/asidenav.js +++ b/frontend/src/utils/asidenav/asidenav.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './asidenav.sass'; const FAVORITES_BTN_CLASS = 'navbar__list-item--favorite'; @@ -15,6 +16,7 @@ export class Asidenav { _element; _asidenavSubmenus; + _eventManager; constructor(element) { if (!element) { @@ -23,6 +25,8 @@ export class Asidenav { this._element = element; + this._eventManager = new EventManager(); + if (this._element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) { return false; } @@ -35,19 +39,24 @@ export class Asidenav { } destroy() { - this._asidenavSubmenus.forEach((union) => { - union.listItem.removeEventListener(union.hoverHandler); - }); + + this._eventManager.removeAllEventListenersFromUtil(); + + if(this._element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) + this._element.classList.remove(ASIDENAV_INITIALIZED_CLASS); } _initFavoritesButton() { const favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS); if (favoritesBtn) { - favoritesBtn.addEventListener('click', (event) => { + + const favoritesButtonEvent = new EventWrapper(EVENT_TYPE.CLICK, (event) => { favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS); this._element.classList.toggle(ASIDENAV_EXPANDED_CLASS); event.preventDefault(); - }, true); + }, favoritesBtn, true); + + this._eventManager.registerNewListener(favoritesButtonEvent); } } @@ -62,7 +71,8 @@ export class Asidenav { this._asidenavSubmenus.forEach((union) => { union.hoverHandler = this._createMouseoverHandler(union); - union.listItem.addEventListener('mouseover', union.hoverHandler); + let currentHoverEvent = new EventWrapper(EVENT_TYPE.MOUSE_OVER, union.hoverHandler, union.listItem); + this._eventManager.registerNewListener(currentHoverEvent); }); } diff --git a/frontend/src/utils/asidenav/asidenav.spec.js b/frontend/src/utils/asidenav/asidenav.spec.js index bdc7aee68..3bd468b28 100644 --- a/frontend/src/utils/asidenav/asidenav.spec.js +++ b/frontend/src/utils/asidenav/asidenav.spec.js @@ -1,4 +1,4 @@ -import { Asidenav } from './asidenav'; +import { Asidenav, ASIDENAV_INITIALIZED_CLASS } from './asidenav'; describe('Asidenav', () => { @@ -13,6 +13,12 @@ describe('Asidenav', () => { expect(asidenav).toBeTruthy(); }); + it('should destory asidenav', () => { + asidenav.destroy(); + expect(asidenav._eventManager._registeredListeners.length).toBe(0); + expect(asidenav._element.classList).not.toContain(ASIDENAV_INITIALIZED_CLASS); + }); + it('should throw if called without an element', () => { expect(() => { new Asidenav(); From e2aa913ff377261cd6f014dd362a4e9888ec7b32 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 28 Jun 2021 09:33:53 +0200 Subject: [PATCH 034/143] chore(async-form): implemented destroy method in async-form --- frontend/src/lib/event-manager/event-manager.js | 1 + frontend/src/utils/async-form/async-form.js | 13 +++++++++++-- frontend/src/utils/async-form/async-form.spec.js | 8 +++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index a73a30fc2..5b590052f 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -5,6 +5,7 @@ export const EVENT_TYPE = { INVALID : 'invalid', CHANGE : 'change', MOUSE_OVER : 'mouseover', + SUBMIT : 'submit', //more to be added }; diff --git a/frontend/src/utils/async-form/async-form.js b/frontend/src/utils/async-form/async-form.js index 6c0da95c0..19e147be8 100644 --- a/frontend/src/utils/async-form/async-form.js +++ b/frontend/src/utils/async-form/async-form.js @@ -1,5 +1,6 @@ import { Utility } from '../../core/utility'; import { Datepicker } from '../form/datepicker'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './async-form.sass'; const ASYNC_FORM_INITIALIZED_CLASS = 'check-all--initialized'; @@ -20,6 +21,8 @@ export class AsyncForm { _element; _app; + _eventManager; + constructor(element, app) { if (!element) { throw new Error('Async Form Utility cannot be setup without an element!'); @@ -28,17 +31,23 @@ export class AsyncForm { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (this._element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS)) { return false; } - this._element.addEventListener('submit', this._submitHandler); + const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, this._submitHandler.bind(this), this._element); + this._eventManager.registerNewListener(submitEvent); this._element.classList.add(ASYNC_FORM_INITIALIZED_CLASS); } destroy() { - // TODO + this._eventManager.removeAllEventListenersFromUtil(); + + if(this._element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS)) + this._element.classList.remove(ASYNC_FORM_INITIALIZED_CLASS); } _processResponse(response) { diff --git a/frontend/src/utils/async-form/async-form.spec.js b/frontend/src/utils/async-form/async-form.spec.js index f01280b8a..aeb7ce4ba 100644 --- a/frontend/src/utils/async-form/async-form.spec.js +++ b/frontend/src/utils/async-form/async-form.spec.js @@ -1,4 +1,4 @@ -import { AsyncForm } from './async-form'; +import { AsyncForm, ASYNC_FORM_INITIALIZED_CLASS } from './async-form'; describe('AsyncForm', () => { @@ -13,6 +13,12 @@ describe('AsyncForm', () => { expect(asyncForm).toBeTruthy(); }); + it('should destroy asyncForm', () => { + asyncForm.destroy(); + expect(asyncForm._eventManager._registeredListeners.length).toBe(0); + expect(asyncForm._element.classList).not.toContain(ASYNC_FORM_INITIALIZED_CLASS); + }); + it('should throw if called without an element', () => { expect(() => { new AsyncForm(); From 29da3d795fead500dba34b0ce0ca92e94d6548ca Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 28 Jun 2021 11:40:10 +0200 Subject: [PATCH 035/143] chore(async-table): implemented destroy method in async table --- .../src/lib/event-manager/event-manager.js | 1 + frontend/src/utils/async-table/async-table.js | 57 +++++++++++++------ .../src/utils/async-table/async-table.spec.js | 9 ++- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index 5b590052f..a1400486a 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -6,6 +6,7 @@ export const EVENT_TYPE = { CHANGE : 'change', MOUSE_OVER : 'mouseover', SUBMIT : 'submit', + INPUT : 'input', //more to be added }; diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index 93d3bcf99..0e3c49685 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -2,6 +2,7 @@ import { Utility } from '../../core/utility'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; import { Datepicker } from '../form/datepicker'; import { HttpClient } from '../../services/http-client/http-client'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import * as debounce from 'lodash.debounce'; import * as throttle from 'lodash.throttle'; import './async-table-filter.sass'; @@ -30,6 +31,8 @@ export class AsyncTable { _element; _app; + _eventManager; + _asyncTableHeader; _asyncTableId; @@ -66,6 +69,8 @@ export class AsyncTable { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) { return false; } @@ -144,7 +149,12 @@ export class AsyncTable { } destroy() { - console.log('TBD: Destroy AsyncTable'); + this._windowStorage.clear(); + this._historyStorage.clear(); + this._eventManager.removeAllEventListenersFromUtil(); + this._active = false; + if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) + this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS); } _startSortableHeaders() { @@ -156,7 +166,8 @@ export class AsyncTable { this._windowStorage.save('horizPos', (this._scrollTable || {}).scrollLeft); this._linkClickHandler(event); }; - th.element.addEventListener('click', th.clickHandler); + const linkClickEvent = new EventWrapper(EVENT_TYPE.CLICK, th.clickHandler.bind(this), th.element); + this._eventManager.registerNewListener(linkClickEvent); }); } @@ -179,7 +190,9 @@ export class AsyncTable { } this._linkClickHandler(event); }; - link.element.addEventListener('click', link.clickHandler); + + const clickEvent = new EventWrapper(EVENT_TYPE.CLICK, link.clickHandler.bind(this), link.element); + this._eventManager.registerNewListener(clickEvent); }); } } @@ -190,7 +203,8 @@ export class AsyncTable { if (this._pagesizeForm) { const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]'); - pagesizeSelect.addEventListener('change', this._changePagesizeHandler); + const pageSizeChangeEvent = new EventWrapper(EVENT_TYPE.CHANGE, this._changePagesizeHandler.bind(this), pagesizeSelect); + this._eventManager.registerNewListener(pageSizeChangeEvent); } } @@ -254,33 +268,42 @@ export class AsyncTable { } }, INPUT_DEBOUNCE); this._cancelPendingUpdates.push(debouncedInput.cancel); - - input.addEventListener('input', () => { + + const inputHandler =() => { this._ignoreRequest = true; debouncedInput(); - }); + }; + const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, inputHandler.bind(this), input ); + this._eventManager.registerNewListener(inputEvent); }); this._tableFilterInputs.change.forEach((input) => { - input.addEventListener('change', () => { + + const changeHandler = () => { //if (this._element.classList.contains(ASYNC_TABLE_LOADING_CLASS)) this._ignoreRequest = true; debouncedUpdateFromTableFilter(); - }); + }; + const changeEvent = new EventWrapper(EVENT_TYPE.CHANGE, changeHandler.bind(this), input); + this._eventManager.registerNewListener(changeEvent); }); this._tableFilterInputs.select.forEach((input) => { - input.addEventListener('change', () => { + const selectChangeHandler = () => { this._ignoreRequest = true; debouncedUpdateFromTableFilter(); - }); + }; + const selectEvent = new EventWrapper(EVENT_TYPE.CHANGE, selectChangeHandler.bind(this), input); + this._eventManager.registerNewListener(selectEvent); }); - tableFilterForm.addEventListener('submit', (event) =>{ + const submitEventHandler = (event) =>{ event.preventDefault(); this._ignoreRequest = true; debouncedUpdateFromTableFilter(); - }); + }; + const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, submitEventHandler.bind(this), tableFilterForm); + this._eventManager.registerNewListener(submitEvent); } _updateFromTableFilter(tableFilterForm) { @@ -439,10 +462,10 @@ export class AsyncTable { ).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS)); } - _debugLog() {} - // _debugLog(fName, ...args) { - // console.log(`[DEBUGLOG] AsyncTable.${fName}`, { args: args, instance: this }); - // } + //_debugLog() {} + _debugLog(fName, ...args) { + console.log(`[DEBUGLOG] AsyncTable.${fName}`, { args: args, instance: this }); + } } diff --git a/frontend/src/utils/async-table/async-table.spec.js b/frontend/src/utils/async-table/async-table.spec.js index 7f008ac49..61b476a9c 100644 --- a/frontend/src/utils/async-table/async-table.spec.js +++ b/frontend/src/utils/async-table/async-table.spec.js @@ -1,4 +1,4 @@ -import { AsyncTable } from './async-table'; +import { AsyncTable, ASYNC_TABLE_INITIALIZED_CLASS } from './async-table'; const AppTestMock = { httpClient: { @@ -50,4 +50,11 @@ describe('AsyncTable', () => { new AsyncTable(); }).toThrow(); }); + + it('should destroy Async Table', () => { + //asyncTable.destroy(); + asyncTable.start(); + expect(asyncTable._eventManager._registeredListeners.length).toBe(0); + expect(asyncTable._element.classList).not.toContain(ASYNC_TABLE_INITIALIZED_CLASS); + }); }); From 4a63050334569d91eea736cf3acab50a23289cfd Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 30 Jun 2021 16:59:45 +0200 Subject: [PATCH 036/143] chore(async-table): implemented destroy in async table --- frontend/src/utils/async-table/async-table.js | 3 +-- frontend/src/utils/async-table/async-table.spec.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index 0e3c49685..68785f276 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -149,8 +149,7 @@ export class AsyncTable { } destroy() { - this._windowStorage.clear(); - this._historyStorage.clear(); + this._windowStorage.clear(this._windowStorage._options); this._eventManager.removeAllEventListenersFromUtil(); this._active = false; if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) diff --git a/frontend/src/utils/async-table/async-table.spec.js b/frontend/src/utils/async-table/async-table.spec.js index 61b476a9c..de5dc9b98 100644 --- a/frontend/src/utils/async-table/async-table.spec.js +++ b/frontend/src/utils/async-table/async-table.spec.js @@ -52,8 +52,8 @@ describe('AsyncTable', () => { }); it('should destroy Async Table', () => { - //asyncTable.destroy(); asyncTable.start(); + asyncTable.destroy(); expect(asyncTable._eventManager._registeredListeners.length).toBe(0); expect(asyncTable._element.classList).not.toContain(ASYNC_TABLE_INITIALIZED_CLASS); }); From c5760bb2ebacb678363bbd88b5a4d1f7992954f3 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 30 Jun 2021 17:00:35 +0200 Subject: [PATCH 037/143] chore(check-all): implemented destroy in check-all --- frontend/src/utils/check-all/check-all.js | 30 +++++++++++++++---- .../src/utils/check-all/check-all.spec.js | 10 ++++++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/frontend/src/utils/check-all/check-all.js b/frontend/src/utils/check-all/check-all.js index b9796eeb5..df151bc62 100644 --- a/frontend/src/utils/check-all/check-all.js +++ b/frontend/src/utils/check-all/check-all.js @@ -2,6 +2,7 @@ const DEBUG_MODE = /localhost/.test(window.location.href) ? 0 : 0; import { Utility } from '../../core/utility'; import { TableIndices } from '../../lib/table/table'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const CHECKBOX_SELECTOR = '[type="checkbox"]'; @@ -13,6 +14,8 @@ const CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized'; export class CheckAll { _element; + _eventManager; + _columns = new Array(); _checkAllColumns = new Array(); @@ -25,6 +28,8 @@ export class CheckAll { this._element = element; + this._eventManager = new EventManager(); + if (this._element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) { return false; } @@ -42,6 +47,16 @@ export class CheckAll { this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS); } + destroy() { + this._eventManager.removeAllEventListenersFromUtil(); + this._checkAllColumns.forEach((column) => { + column._checkAllCheckBox.remove(); + }); + + if(this._element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) + this._element.classList.remove(CHECK_ALL_INITIALIZED_CLASS); + } + _gatherColumns() { for (const rowIndex of Array(this._tableIndices.maxRow + 1).keys()) { for (const colIndex of Array(this._tableIndices.maxCol + 1).keys()) { @@ -81,14 +96,17 @@ class CheckAllColumn { _app; _table; _column; + + _eventManager _checkAllCheckbox; _checkboxId = 'check-all-checkbox-' + Math.floor(Math.random() * 100000); - constructor(table, app, column) { + constructor(table, app, column, eventManager) { this._column = column; this._table = table; this._app = app; + this._eventManager = eventManager; const th = this._column.filter(element => element.tagName == 'TH')[0]; if (!th) @@ -102,7 +120,8 @@ class CheckAllColumn { // set up new checkbox this._app.utilRegistry.initAll(th); - this._checkAllCheckbox.addEventListener('input', this._onCheckAllCheckboxInput.bind(this)); + const checkBoxInputEvent = new EventWrapper(EVENT_TYPE.INPUT, this._onCheckAllCheckboxInput.bind(this), this._checkAllCheckbox); + this._eventManager.registerNewListener(checkBoxInputEvent); this._setupCheckboxListeners(); } @@ -113,9 +132,10 @@ class CheckAllColumn { _setupCheckboxListeners() { this._column .flatMap(cell => cell.tagName == 'TH' ? new Array() : Array.from(cell.querySelectorAll(CHECKBOX_SELECTOR))) - .forEach(checkbox => - checkbox.addEventListener('input', this._updateCheckAllCheckboxState.bind(this)) - ); + .forEach(checkbox => { + const checkBoxUpdateEvent = new EventWrapper(EVENT_TYPE.INPUT, this._updateCheckAllCheckboxState.bind(this), checkbox); + this._eventManager.registerNewListener(checkBoxUpdateEvent); + }); } _updateCheckAllCheckboxState() { diff --git a/frontend/src/utils/check-all/check-all.spec.js b/frontend/src/utils/check-all/check-all.spec.js index 9b8d30f77..08cf2a088 100644 --- a/frontend/src/utils/check-all/check-all.spec.js +++ b/frontend/src/utils/check-all/check-all.spec.js @@ -1,4 +1,4 @@ -import { CheckAll } from './check-all'; +import { CheckAll, CHECK_ALL_INITIALIZED_CLASS } from './check-all'; const MOCK_APP = { utilRegistry: { @@ -24,4 +24,12 @@ describe('CheckAll', () => { new CheckAll(); }).toThrow(); }); + + it('should destroy CheckAll', () => { + checkAll.destroy(); + expect(checkAll._eventManager._registeredListeners.length).toBe(0); + console.log(checkAll._element.classList); + expect(checkAll._element.classList).not.toEqual(jasmine.arrayContaining([CHECK_ALL_INITIALIZED_CLASS])); + }); + }); From 6d66b822ac59cf751a2c67bfa366494c91ce04bf Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 30 Jun 2021 17:01:20 +0200 Subject: [PATCH 038/143] chore(course-teaser): implemented destroy in course teaser --- .../src/utils/course-teaser/course-teaser.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/course-teaser/course-teaser.js b/frontend/src/utils/course-teaser/course-teaser.js index 0419dcbda..95a49faee 100644 --- a/frontend/src/utils/course-teaser/course-teaser.js +++ b/frontend/src/utils/course-teaser/course-teaser.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './course-teaser.sass'; const COURSE_TEASER_INITIALIZED_CLASS = 'course-teaser--initialized'; @@ -12,16 +13,30 @@ const COURSE_TEASER_CHEVRON_CLASS = 'course-teaser__chevron'; export class CourseTeaser { _element; + _eventManager constructor(element) { if (!element) { throw new Error('CourseTeaser utility cannot be setup without an element!'); } + this._eventManager = new EventManager(); if (element.classList.contains(COURSE_TEASER_INITIALIZED_CLASS)) { return false; } this._element = element; - element.addEventListener('click', e => this._onToggleExpand(e)); + const clickHandler = e => this._onToggleExpand(e); + const clickEvent = new EventWrapper(EVENT_TYPE.CLICK, clickHandler.bind(this), element); + this._eventManager.registerNewListener(clickEvent); + } + + destroy() { + this._eventManager.removeAllEventListenersFromUtil(); + if(this._element.classList.contains(COURSE_TEASER_EXPANDED_CLASS)) { + this._element.classList.remove(COURSE_TEASER_EXPANDED_CLASS); + } + if (this._element.classList.contains(COURSE_TEASER_INITIALIZED_CLASS)) { + this._element.classList.remove(COURSE_TEASER_INITIALIZED_CLASS); + } } _onToggleExpand(event) { From 780a5f7ce17fbe419cedc077e196e3999c929212 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 2 Jul 2021 15:27:33 +0200 Subject: [PATCH 039/143] chore(exam-correct): destroy method in exam-correct implemented --- .../src/lib/event-manager/event-manager.js | 1 + .../src/utils/check-all/check-all.spec.js | 1 - .../src/utils/exam-correct/exam-correct.js | 42 ++++++++++++------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index a1400486a..f0959d66e 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -7,6 +7,7 @@ export const EVENT_TYPE = { MOUSE_OVER : 'mouseover', SUBMIT : 'submit', INPUT : 'input', + FOCUS_OUT : 'focusout', //more to be added }; diff --git a/frontend/src/utils/check-all/check-all.spec.js b/frontend/src/utils/check-all/check-all.spec.js index 08cf2a088..431dd5993 100644 --- a/frontend/src/utils/check-all/check-all.spec.js +++ b/frontend/src/utils/check-all/check-all.spec.js @@ -28,7 +28,6 @@ describe('CheckAll', () => { it('should destroy CheckAll', () => { checkAll.destroy(); expect(checkAll._eventManager._registeredListeners.length).toBe(0); - console.log(checkAll._element.classList); expect(checkAll._element.classList).not.toEqual(jasmine.arrayContaining([CHECK_ALL_INITIALIZED_CLASS])); }); diff --git a/frontend/src/utils/exam-correct/exam-correct.js b/frontend/src/utils/exam-correct/exam-correct.js index cf23c911d..c88ce2d6b 100644 --- a/frontend/src/utils/exam-correct/exam-correct.js +++ b/frontend/src/utils/exam-correct/exam-correct.js @@ -1,5 +1,6 @@ import { Utility } from '../../core/utility'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import { HttpClient } from '../../services/http-client/http-client'; import moment from 'moment'; @@ -58,6 +59,7 @@ export class ExamCorrect { _lastColumnIndex; _storageManager; + _eventManager; constructor(element, app) { if (!element) { @@ -71,6 +73,8 @@ export class ExamCorrect { this._element = element; this._app = app; + this._eventManager = new EventManager(); + // TODO work in progress // this._storageManager = new StorageManager('EXAM_CORRECT', '0.0.0', { location: LOCATION.SESSION, encryption: { all: { tag: 'exam-correct', exam: this._element.getAttribute('uw-exam-correct') } } }); this._storageManager = new StorageManager('EXAM_CORRECT', '0.0.0', { location: LOCATION.WINDOW }); @@ -88,20 +92,28 @@ export class ExamCorrect { this._resultPassSelect = resultDetailCell && resultDetailCell.querySelector('select.uw-exam-correct__pass'); this._partDeleteBoxes = [...this._element.querySelectorAll('input.uw-exam-correct--delete-exam-part')]; - if (this._sendBtn) - this._sendBtn.addEventListener('click', this._sendCorrectionHandler.bind(this)); - else console.error('ExamCorrect utility could not detect send button!'); + if (this._sendBtn){ + const sendClickEvent = new EventWrapper(EVENT_TYPE.CLICK, this._sendCorrectionHandler.bind(this), this._sendBtn); + this._eventManager.registerNewListener(sendClickEvent); + } else { + console.error('ExamCorrect utility could not detect send button!'); + } - if (this._userInput) - this._userInput.addEventListener('focusout', this._validateUserInput.bind(this)); - else throw new Error('ExamCorrect utility could not detect user input!'); + if (this._userInput) { + const focusOutEvent = new EventWrapper(EVENT_TYPE.FOCUS_OUT, this._validateUserInput.bind(this), this._userInput); + this._eventManager.registerNewListener(focusOutEvent); + } else { + throw new Error('ExamCorrect utility could not detect user input!'); + } for (let deleteBox of this._partDeleteBoxes) { - deleteBox.addEventListener('change', (() => { this._updatePartDeleteDisabled(deleteBox); }).bind(this)); + const deleteBoxChangeEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { this._updatePartDeleteDisabled(deleteBox); }).bind(this), deleteBox); + this._eventManger.registerNewListener(deleteBoxChangeEvent); } for (let input of [this._userInput, ...this._partInputs]) { - input.addEventListener('keypress', this._inputKeypress.bind(this)); + const inputKeyDownEvent = new EventWrapper(EVENT_TYPE.KEYDOWN, this._inputKeypress.bind(this), input); + this._eventManager.registerNewListener(inputKeyDownEvent); } if (!this._userInputStatus) { @@ -125,23 +137,25 @@ export class ExamCorrect { ); if (this._resultSelect && this._resultGradeSelect) { - this._resultSelect.addEventListener('change', () => { + const resultSelectEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { if (this._resultSelect.value !== 'grade') this._resultGradeSelect.classList.add('grade-hidden'); else this._resultGradeSelect.classList.remove('grade-hidden'); - }); + }).bind(this), this._resultSelect ); + this._eventManager.registerNewListener(resultSelectEvent); if (this._resultSelect.value !== 'grade') this._resultGradeSelect.classList.add('grade-hidden'); } if (this._resultSelect && this._resultPassSelect) { - this._resultSelect.addEventListener('change', () => { + const resultPassSelectEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { if (this._resultSelect.value !== 'pass') this._resultPassSelect.classList.add('pass-hidden'); else this._resultPassSelect.classList.remove('pass-hidden'); - }); + }).bind(this), this._resultSelect); + this._eventManager.registerNewListener(resultPassSelectEvent); if (this._resultSelect.value !== 'pass') this._resultPassSelect.classList.add('pass-hidden'); @@ -158,9 +172,7 @@ export class ExamCorrect { } destroy() { - this._sendBtn.removeEventListener('click', this._sendCorrectionHandler); - this._userInput.removeEventListener('change', this._validateUserInput); - // TODO destroy handlers on user input candidate elements + this._eventManager.removeAllEventListenersFromUtil(); } _updatePartDeleteDisabled(deleteBox) { From 3e01c8d9102af258502b2d6352503853109b25ea Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 2 Jul 2021 15:40:53 +0200 Subject: [PATCH 040/143] chore(auto-submit-button): implemented destroy in auto-submit-button --- frontend/src/utils/form/auto-submit-button.js | 5 +++- .../src/utils/form/auto-submit-button.spec.js | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 frontend/src/utils/form/auto-submit-button.spec.js diff --git a/frontend/src/utils/form/auto-submit-button.js b/frontend/src/utils/form/auto-submit-button.js index bf7544d30..77e942f28 100644 --- a/frontend/src/utils/form/auto-submit-button.js +++ b/frontend/src/utils/form/auto-submit-button.js @@ -9,8 +9,10 @@ const AUTO_SUBMIT_BUTTON_HIDDEN_CLASS = 'hidden'; selector: AUTO_SUBMIT_BUTTON_UTIL_SELECTOR, }) export class AutoSubmitButton { + _element; constructor(element) { + this._element = element; if (!element) { throw new Error('Auto Submit Button utility needs to be passed an element!'); } @@ -24,6 +26,7 @@ export class AutoSubmitButton { } destroy() { - // TODO + this._element.classList.remove(AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS); + this._element.classList.remove(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS); } } diff --git a/frontend/src/utils/form/auto-submit-button.spec.js b/frontend/src/utils/form/auto-submit-button.spec.js new file mode 100644 index 000000000..b1436c867 --- /dev/null +++ b/frontend/src/utils/form/auto-submit-button.spec.js @@ -0,0 +1,27 @@ +import { AutoSubmitButton, AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS, AUTO_SUBMIT_BUTTON_HIDDEN_CLASS } from './auto-submit-button.js'; + +describe('Auto-submit-button', () => { + + let autoSubmitButton; + + beforeEach(() => { + const element = document.createElement('div'); + autoSubmitButton = new AutoSubmitButton(element); + }); + + it('should create', () => { + expect(autoSubmitButton).toBeTruthy(); + }); + + it('should destory auto-submit-button', () => { + autoSubmitButton.destroy(); + expect(autoSubmitButton._element.classList).not.toContain(AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS); + expect(autoSubmitButton._element.classList).not.toContain(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS); + }); + + it('should throw if called without an element', () => { + expect(() => { + new AutoSubmitButton(); + }).toThrow(); + }); + }); \ No newline at end of file From a75f6621ce56aabd52241fa9ee91b61886cb409e Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 2 Jul 2021 16:43:53 +0200 Subject: [PATCH 041/143] chore(auto-submit-input): implemented destroy in auto-submit-input --- frontend/src/utils/form/auto-submit-input.js | 13 ++++++-- .../src/utils/form/auto-submit-input.spec.js | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 frontend/src/utils/form/auto-submit-input.spec.js diff --git a/frontend/src/utils/form/auto-submit-input.js b/frontend/src/utils/form/auto-submit-input.js index f442f2960..8ec7e869e 100644 --- a/frontend/src/utils/form/auto-submit-input.js +++ b/frontend/src/utils/form/auto-submit-input.js @@ -1,5 +1,6 @@ import * as debounce from 'lodash.debounce'; import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; export const AUTO_SUBMIT_INPUT_UTIL_SELECTOR = '[uw-auto-submit-input]'; @@ -12,6 +13,8 @@ export class AutoSubmitInput { _element; + _eventManager; + _form; _debouncedHandler; @@ -22,6 +25,8 @@ export class AutoSubmitInput { this._element = element; + this._eventManager = new EventManager(); + if (this._element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS)) { return false; } @@ -33,12 +38,16 @@ export class AutoSubmitInput { this._debouncedHandler = debounce(this.autoSubmit, 500); - this._element.addEventListener('input', this._debouncedHandler); + const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, this._debouncedHandler.bind(this), this._element); + this._eventManager.registerNewListener(inputEvent); + this._element.classList.add(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS); } destroy() { - this._element.removeEventListener('input', this._debouncedHandler); + this._eventManager.removeAllEventListenersFromUtil(); + if(this._element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS)) + this._element.classList.remove(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS); } autoSubmit = () => { diff --git a/frontend/src/utils/form/auto-submit-input.spec.js b/frontend/src/utils/form/auto-submit-input.spec.js new file mode 100644 index 000000000..26c59cdd2 --- /dev/null +++ b/frontend/src/utils/form/auto-submit-input.spec.js @@ -0,0 +1,30 @@ +import { AutoSubmitInput, AUTO_SUBMIT_INPUT_INITIALIZED_CLASS } from './auto-submit-input.js'; + +describe('Auto-submit-input', () => { + + let autoSubmitInput; + + beforeEach(() => { + const form = document.createElement('form'); + const element = document.createElement('input'); + element.setAttribute('type', 'text'); + form.append(element); + autoSubmitInput = new AutoSubmitInput(element); + }); + + it('should create', () => { + expect(autoSubmitInput).toBeTruthy(); + }); + + it('should destory auto-submit-button', () => { + autoSubmitInput.destroy(); + expect(autoSubmitInput._eventManager._registeredListeners.length).toBe(0); + expect(autoSubmitInput._element.classList).not.toEqual(jasmine.arrayContaining([AUTO_SUBMIT_INPUT_INITIALIZED_CLASS])); + }); + + it('should throw if called without an element', () => { + expect(() => { + new AutoSubmitInput(); + }).toThrow(); + }); + }); \ No newline at end of file From 6581137d2cddf696456b8e894c04a3a4614104dc Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 5 Jul 2021 10:47:04 +0200 Subject: [PATCH 042/143] chore(communication-recipients): implemented destroy method --- .../utils/form/communication-recipients.js | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/frontend/src/utils/form/communication-recipients.js b/frontend/src/utils/form/communication-recipients.js index 2a9367f47..cf28715eb 100644 --- a/frontend/src/utils/form/communication-recipients.js +++ b/frontend/src/utils/form/communication-recipients.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const MASS_INPUT_SELECTOR = '.massinput'; const RECIPIENT_CATEGORIES_SELECTOR = '.recipient-categories'; @@ -14,12 +15,16 @@ const RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR = '.recipient-category__checke }) export class CommunicationRecipients { massInputElement; + _element; + + _eventManager; constructor(element) { if (!element) { throw new Error('Communication Recipient utility cannot be setup without an element!'); } - + this._element = element; + this._eventManager = new EventManager(); this.massInputElement = element.closest(MASS_INPUT_SELECTOR); this.setupRecipientCategories(); @@ -31,6 +36,18 @@ export class CommunicationRecipients { setupRecipientCategories() { Array.from(this.massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR)).forEach(setupRecipientCategory); } + + removeCheckedCounter() { + let checkedCounters = this._element.querySelectorAll(RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR); + checkedCounters.forEach((checkedCounter) => { + checkedCounter.innerHTML = ''; + }); + } + + destroy() { + this._eventManager.removeAllEventListenersFromUtil(); + this.removeCheckedCounter(); + } } function setupRecipientCategory(recipientCategoryElement) { @@ -42,34 +59,35 @@ function setupRecipientCategory(recipientCategoryElement) { const toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR); // setup category checkbox to toggle all child checkboxes if changed - categoryCheckbox.addEventListener('change', () => { + const categoryToggleEvent = new EventWrapper(EVENT_TYPE.CHANGE,(() => { categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => { checkbox.checked = categoryCheckbox.checked; }); updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - }); + }).bind(this), categoryCheckbox ); + this._eventManager.registerNewListener(categoryToggleEvent); // update counter and toggle checkbox initially updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); // register change listener for individual checkboxes - categoryCheckboxes.forEach(checkbox => { - checkbox.addEventListener('change', () => { - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - }); - }); + const individualToggleEvent = new EventWrapper(EVENT_TYPE.CHANGE,(() => { + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); + }).bind(this), categoryCheckboxes ); + this._eventManager.registerNewListener(individualToggleEvent); // register change listener for toggle all checkbox if (toggleAllCheckbox) { - toggleAllCheckbox.addEventListener('change', () => { + const toggleAllEvent = new EventWrapper(EVENT_TYPE.CHANG, (() => { categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => { checkbox.checked = toggleAllCheckbox.checked; }); updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - }); + }).bind(this), toggleAllCheckbox); + this._eventManager.registerNewListener(toggleAllEvent); } } } From 34b4f48386c8fff4569b12eb3f7919e7c77d33c0 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 5 Jul 2021 12:01:27 +0200 Subject: [PATCH 043/143] feat(event-manager): mutation observers can be managed via the event manager --- frontend/src/lib/event-manager/event-manager.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index f0959d66e..a091445d5 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -15,10 +15,12 @@ export const EVENT_TYPE = { export class EventManager { _registeredListeners; + _mutationObservers; constructor() { this._registeredListeners = []; + this._mutationObservers = []; } registerNewListener(eventWrapper) { @@ -28,6 +30,12 @@ export class EventManager { this._registeredListeners.push(eventWrapper); } + registerNewMutationObserver(callback, domNode, config) { + let observer = new MutationObserver(callback); + observer.observe(domNode, config); + this._muatationObservers.add(observer); + } + removeAllEventListenersFromUtil() { this._debugLog('removeAllEventListenersFromUtil',); for (let eventWrapper of this._registeredListeners) { @@ -37,6 +45,10 @@ export class EventManager { this._registeredListeners = []; } + removeAllObserversFromUtil() { + this._mutationObservers.forEach((observer) => observer.disconnect()); + } + //Todo: Uncomment debug log! //_debugLog() {} From 5af045c11bd1b9b13f262b1029fafd4198d5229c Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 5 Jul 2021 12:05:16 +0200 Subject: [PATCH 044/143] chore(communication-recipients): mutation observer is managed via eventManager --- frontend/src/utils/form/communication-recipients.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/form/communication-recipients.js b/frontend/src/utils/form/communication-recipients.js index cf28715eb..7848a5736 100644 --- a/frontend/src/utils/form/communication-recipients.js +++ b/frontend/src/utils/form/communication-recipients.js @@ -29,8 +29,7 @@ export class CommunicationRecipients { this.setupRecipientCategories(); - const recipientObserver = new MutationObserver(this.setupRecipientCategories.bind(this)); - recipientObserver.observe(this.massInputElement, { childList: true }); + this._eventManager.registerNewMutationObserver(this.setupRecipientCategories.bind(this), this.massInputElement, { childList: true }); } setupRecipientCategories() { @@ -46,6 +45,7 @@ export class CommunicationRecipients { destroy() { this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.removeAllObserversFromUtil(); this.removeCheckedCounter(); } } From 9e342a5368a69122f3d779a500052e1953313027 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 5 Jul 2021 15:00:44 +0200 Subject: [PATCH 045/143] chore(datpicker): implemented destroy method --- frontend/src/utils/form/datepicker.js | 36 +++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/frontend/src/utils/form/datepicker.js b/frontend/src/utils/form/datepicker.js index 21b09eb19..7a8abe3ad 100644 --- a/frontend/src/utils/form/datepicker.js +++ b/frontend/src/utils/form/datepicker.js @@ -2,6 +2,7 @@ import datetime from 'tail.datetime'; import './datepicker.css'; import { Utility } from '../../core/utility'; import moment from 'moment'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import * as defer from 'lodash.defer'; @@ -82,6 +83,8 @@ export class Datepicker { initialValue; _locale; + _eventManager; + _unloadIsDueToSubmit = false; constructor(element) { @@ -102,6 +105,8 @@ export class Datepicker { this._element = element; + this._eventManager = new EventManager(); + // store the previously set type to select the input format this.elementType = this._element.getAttribute('type'); @@ -179,23 +184,22 @@ export class Datepicker { } // reregister change event to prevent event loop - this._element.addEventListener('change', setDatepickerDate, { once: true }); }; // change the selected date in the tail.datetime instance if the value of the input element is changed - this._element.addEventListener('change', setDatepickerDate, { once: true }); - + const changeSelectedDateEvent = new EventWrapper(EVENT_TYPE.CHANGE, setDatepickerDate.bind(this), this._element, { once: true }); + this._eventManager.registerNewListener(changeSelectedDateEvent); // create a mutation observer that observes the datepicker instance class and sets // the datepicker-open DOM attribute of the input element if the datepicker has been opened - const datepickerInstanceObserver = new MutationObserver((mutations) => { + let callback = (mutations) => { for (const mutation of mutations) { if (!mutation.oldValue.includes(DATEPICKER_OPEN_CLASS) && this.datepickerInstance.dt.getAttribute('class').includes(DATEPICKER_OPEN_CLASS)) { this._element.setAttribute(ATTR_DATEPICKER_OPEN, true); break; } } - }); - datepickerInstanceObserver.observe(this.datepickerInstance.dt, { + }; + this._eventManager.registerNewMutationObserver(callback.bind(this), this.datepickerInstance.dt, { attributes: true, attributeFilter: ['class'], attributeOldValue: true, @@ -203,38 +207,44 @@ export class Datepicker { // close the instance on focusout of any element if another input is focussed that is neither the timepicker nor _element - window.addEventListener('focusout', event => { + const focusOutEvent = new EventWrapper(EVENT_TYPE.FOCUS_OUT,(event => { const hasFocus = event.relatedTarget !== null; const focussedIsNotTimepicker = !this.datepickerInstance.dt.contains(event.relatedTarget); const focussedIsNotElement = event.relatedTarget !== this._element; const focussedIsInDocument = window.document.contains(event.relatedTarget); if (hasFocus && focussedIsNotTimepicker && focussedIsNotElement && focussedIsInDocument) this.closeDatepickerInstance(); - }); + }).bind(this), window ); + this._eventManager.registerNewListener(focusOutEvent); // close the instance on click on any element outside of the datepicker (except the input element itself) - window.addEventListener('click', event => { + const clickOutsideEvent = new EventWrapper(EVENT_TYPE.CLICK, (event => { const targetIsOutside = !this.datepickerInstance.dt.contains(event.target) && event.target !== this.datepickerInstance.dt; const targetIsInDocument = window.document.contains(event.target); const targetIsNotElement = event.target !== this._element; if (targetIsOutside && targetIsInDocument && targetIsNotElement) this.closeDatepickerInstance(); - }); + }).bind(this), window); + this._eventManager.registerNewListener(clickOutsideEvent); // close the instance on escape keydown events - this._element.addEventListener('keydown', event => { + const escapeCloseEvent = new EventWrapper(EVENT_TYPE.KEYDOWN, (event => { if (event.keyCode === KEYCODE_ESCAPE) { this.closeDatepickerInstance(); } - }); + }).bind(this), this._element); + this._eventManager.registerNewListener(escapeCloseEvent); // format the date value of the form input element of this datepicker before form submission - this._element.form.addEventListener('submit', this._submitHandler.bind(this)); + const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, this._submitHandler.bind(this), this._element.form); + this._eventManager.registerNewListener(submitEvent); } destroy() { this.datepickerInstance.remove(); + this._eventManager.removeAllListenersFromUtil(); + this._element.classList.remove(DATEPICKER_INITIALIZED_CLASS); } From 03ac80342e0f3fcb1db2adf34f86a2c0d8fabf0f Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Thu, 8 Jul 2021 12:17:25 +0200 Subject: [PATCH 046/143] fix(communication-recipients): fixed undefined error with context and a few minor issues --- .../src/lib/event-manager/event-manager.js | 2 +- .../utils/form/communication-recipients.js | 81 ++++++++++--------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index a091445d5..16bc9daa9 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -33,7 +33,7 @@ export class EventManager { registerNewMutationObserver(callback, domNode, config) { let observer = new MutationObserver(callback); observer.observe(domNode, config); - this._muatationObservers.add(observer); + this._mutationObservers.push(observer); } removeAllEventListenersFromUtil() { diff --git a/frontend/src/utils/form/communication-recipients.js b/frontend/src/utils/form/communication-recipients.js index 7848a5736..dc04f17cc 100644 --- a/frontend/src/utils/form/communication-recipients.js +++ b/frontend/src/utils/form/communication-recipients.js @@ -25,7 +25,7 @@ export class CommunicationRecipients { } this._element = element; this._eventManager = new EventManager(); - this.massInputElement = element.closest(MASS_INPUT_SELECTOR); + this.massInputElement = this._element.closest(MASS_INPUT_SELECTOR); this.setupRecipientCategories(); @@ -33,10 +33,10 @@ export class CommunicationRecipients { } setupRecipientCategories() { - Array.from(this.massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR)).forEach(setupRecipientCategory); + Array.from(this.massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR)).forEach(this.setupRecipientCategory.bind(this)); } - removeCheckedCounter() { + _removeCheckedCounter() { let checkedCounters = this._element.querySelectorAll(RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR); checkedCounters.forEach((checkedCounter) => { checkedCounter.innerHTML = ''; @@ -46,48 +46,51 @@ export class CommunicationRecipients { destroy() { this._eventManager.removeAllEventListenersFromUtil(); this._eventManager.removeAllObserversFromUtil(); - this.removeCheckedCounter(); + this._removeCheckedCounter(); } -} -function setupRecipientCategory(recipientCategoryElement) { - const categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR); - const categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR); - if (categoryOptions) { - const categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]')); - const toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR); - - // setup category checkbox to toggle all child checkboxes if changed - const categoryToggleEvent = new EventWrapper(EVENT_TYPE.CHANGE,(() => { - categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => { - checkbox.checked = categoryCheckbox.checked; - }); - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - }).bind(this), categoryCheckbox ); - this._eventManager.registerNewListener(categoryToggleEvent); - - // update counter and toggle checkbox initially - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - - // register change listener for individual checkboxes - const individualToggleEvent = new EventWrapper(EVENT_TYPE.CHANGE,(() => { - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - }).bind(this), categoryCheckboxes ); - this._eventManager.registerNewListener(individualToggleEvent); - - // register change listener for toggle all checkbox - if (toggleAllCheckbox) { - const toggleAllEvent = new EventWrapper(EVENT_TYPE.CHANG, (() => { + setupRecipientCategory(recipientCategoryElement) { + const categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR); + const categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR); + + if (categoryOptions) { + const categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]')); + const toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR); + + // setup category checkbox to toggle all child checkboxes if changed + const categoryToggleEvent = new EventWrapper(EVENT_TYPE.CHANGE,(() => { categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => { - checkbox.checked = toggleAllCheckbox.checked; + checkbox.checked = categoryCheckbox.checked; }); updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - }).bind(this), toggleAllCheckbox); - this._eventManager.registerNewListener(toggleAllEvent); + updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); + }).bind(this), categoryCheckbox ); + this._eventManager.registerNewListener(categoryToggleEvent); + + // update counter and toggle checkbox initially + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); + + // register change listener for individual checkboxes + categoryCheckboxes.forEach(checkbox => { + const individualToggleEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); + }).bind(this), checkbox); + this._eventManager.registerNewListener(individualToggleEvent); + }); + + // register change listener for toggle all checkbox + if (toggleAllCheckbox) { + const toggleAllEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { + categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => { + checkbox.checked = toggleAllCheckbox.checked; + }); + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + }).bind(this), toggleAllCheckbox); + this._eventManager.registerNewListener(toggleAllEvent); + } } } } From edc998288a42957ebf6515d94ae82d694fbaad55 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Thu, 8 Jul 2021 12:31:24 +0200 Subject: [PATCH 047/143] chore(form-error-remover): implemented destroy --- frontend/src/utils/form/form-error-remover.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/form/form-error-remover.js b/frontend/src/utils/form/form-error-remover.js index 1b1509c2d..5fc1f90c0 100644 --- a/frontend/src/utils/form/form-error-remover.js +++ b/frontend/src/utils/form/form-error-remover.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const FORM_ERROR_REMOVER_INITIALIZED_CLASS = 'form-error-remover--initialized'; const FORM_ERROR_REMOVER_INPUTS_SELECTOR = 'input:not([type="hidden"]), textarea, select'; @@ -13,6 +14,8 @@ export class FormErrorRemover { _element; + _eventManager; + constructor(element) { if (!element) throw new Error('Form Error Remover utility needs to be passed an element!'); @@ -24,6 +27,7 @@ export class FormErrorRemover { return; this._element = element; + this._eventManager = new EventManager(); this._element.classList.add(FORM_ERROR_REMOVER_INITIALIZED_CLASS); } @@ -35,11 +39,18 @@ export class FormErrorRemover { const inputElements = Array.from(this._element.querySelectorAll(FORM_ERROR_REMOVER_INPUTS_SELECTOR)); inputElements.forEach((inputElement) => { - inputElement.addEventListener('input', () => { + const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, (() => { if (!inputElement.willValidate || inputElement.validity.vaild) { FORM_GROUP_WITH_ERRORS_CLASSES.forEach(c => { this._element.classList.remove(c); }); } - }); + }).bind(this), inputElement); + this._eventManager.registerNewListener(inputEvent); }); } + + destroy() { + this._eventManager.removeAllEventListenersFromUtil(); + this._element.classList.remove(FORM_ERROR_REMOVER_INITIALIZED_CLASS); + } + } From 266ceea5a84774ca5c266708b98107a93071aa02 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Thu, 8 Jul 2021 12:39:36 +0200 Subject: [PATCH 048/143] chore(enter-is-tab): added readme.md file for util --- frontend/src/utils/form/enter-is-tab.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 frontend/src/utils/form/enter-is-tab.md diff --git a/frontend/src/utils/form/enter-is-tab.md b/frontend/src/utils/form/enter-is-tab.md new file mode 100644 index 000000000..67fd7e2c4 --- /dev/null +++ b/frontend/src/utils/form/enter-is-tab.md @@ -0,0 +1,8 @@ +# Enter is Tab Utility +When the user presses enter on a form that uses this utility, the enter is converted to a tab in order to not send the form. + +## Attribute: +`uw-enter-as-tab` + +## Example usage: + \ No newline at end of file From 40283e348b2df6b1520164ba3541d01d72c8ea3c Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Thu, 8 Jul 2021 15:53:42 +0200 Subject: [PATCH 049/143] chore(interactive-fieldset): implemented destroy method --- frontend/src/utils/form/interactive-fieldset.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/utils/form/interactive-fieldset.js b/frontend/src/utils/form/interactive-fieldset.js index 7e912ab90..70051b64b 100644 --- a/frontend/src/utils/form/interactive-fieldset.js +++ b/frontend/src/utils/form/interactive-fieldset.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR = '.interactive-fieldset__target'; @@ -15,6 +16,8 @@ export class InteractiveFieldset { _element; + _eventManager; + conditionalInput; conditionalValue; target; @@ -28,6 +31,8 @@ export class InteractiveFieldset { this._element = element; + this._eventManger = new EventManager(); + if (this._element.classList.contains(INTERACTIVE_FIELDSET_INITIALIZED_CLASS)) { return false; } @@ -62,13 +67,11 @@ export class InteractiveFieldset { this.childInputs = Array.from(this._element.querySelectorAll(INTERACTIVE_FIELDSET_CHILD_SELECTOR)).filter(child => child.closest('[uw-interactive-fieldset]') === this._element); // add event listener - const observer = new MutationObserver(this._updateVisibility.bind(this)); - observer.observe(this.conditionalInput, { attributes: true, attributeFilter: ['data-interactive-fieldset-hidden'] }); - this.conditionalInput.addEventListener('input', this._updateVisibility.bind(this)); - + this._eventManager.registerNewMutationObserver(this._updateVisibility.bind(this), this.conditionalInput, { attributes: true, attributeFilter: ['data-interactive-fieldset-hidden'] }); + const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, this._updateVisibility.bind(this), this.conditionalInput); + this._eventManager.registerNewListener(inputEvent); // mark as initialized this._element.classList.add(INTERACTIVE_FIELDSET_INITIALIZED_CLASS); - } start() { @@ -77,7 +80,8 @@ export class InteractiveFieldset { } destroy() { - // TODO + this._eventManager.cleanUp(); + this._element.classList.remove(INTERACTIVE_FIELDSET_INITIALIZED_CLASS); } _updateVisibility() { From 4c2c68327e75f5f51271853159c232fdd7bba21e Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 13 Jul 2021 12:39:51 +0200 Subject: [PATCH 050/143] fix(interactive-fieldset): small fix --- frontend/src/lib/event-manager/event-manager.js | 8 +++++++- frontend/src/utils/form/interactive-fieldset.js | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index 16bc9daa9..35a5075e6 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -8,7 +8,7 @@ export const EVENT_TYPE = { SUBMIT : 'submit', INPUT : 'input', FOCUS_OUT : 'focusout', - //more to be added + BEFOREUNLOAD : 'beforeunload', }; @@ -47,6 +47,12 @@ export class EventManager { removeAllObserversFromUtil() { this._mutationObservers.forEach((observer) => observer.disconnect()); + this.mutationObservers = []; + } + + cleanUp() { + this.removeAllObserversFromUtil(); + this.removeAllEventListenersFromUtil(); } diff --git a/frontend/src/utils/form/interactive-fieldset.js b/frontend/src/utils/form/interactive-fieldset.js index 70051b64b..2c8ff4eb9 100644 --- a/frontend/src/utils/form/interactive-fieldset.js +++ b/frontend/src/utils/form/interactive-fieldset.js @@ -31,7 +31,7 @@ export class InteractiveFieldset { this._element = element; - this._eventManger = new EventManager(); + this._eventManager = new EventManager(); if (this._element.classList.contains(INTERACTIVE_FIELDSET_INITIALIZED_CLASS)) { return false; From 2f668c1d3ef891bc9fd682e89875d8030ea860c9 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 13 Jul 2021 12:41:01 +0200 Subject: [PATCH 051/143] chore(navigate-away-prompt): implemnted destroy method --- frontend/src/utils/form/navigate-away-prompt.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/src/utils/form/navigate-away-prompt.js b/frontend/src/utils/form/navigate-away-prompt.js index 8890e198e..9c6a57eee 100644 --- a/frontend/src/utils/form/navigate-away-prompt.js +++ b/frontend/src/utils/form/navigate-away-prompt.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import { AUTO_SUBMIT_BUTTON_UTIL_SELECTOR } from './auto-submit-button'; import { AUTO_SUBMIT_INPUT_UTIL_SELECTOR } from './auto-submit-input'; @@ -27,6 +28,8 @@ export class NavigateAwayPrompt { _element; + _eventManager; + _initFormData; _unloadDueToSubmit = false; @@ -36,6 +39,7 @@ export class NavigateAwayPrompt { } this._element = element; + this._eventManager = new EventManager(); if (this._element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) { return; @@ -65,15 +69,18 @@ export class NavigateAwayPrompt { return; this._initFormData = new FormData(this._element); - window.addEventListener('beforeunload', this._beforeUnloadHandler.bind(this)); + const beforeUnloadEvent = new EventWrapper(EVENT_TYPE.BEFOREUNLOAD, this._beforeUnloadHandler.bind(this), window); + this._eventManager.registerNewListener(beforeUnloadEvent); - this._element.addEventListener('submit', () => { + const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, (() => { this._unloadDueToSubmit = true; defer(() => { this._unloadDueToSubmit = false; } ); // Restore state after event loop is settled - }); + }).bind(this), this._element); + this._eventManager.registerNewListener(submitEvent); } destroy() { + this._eventManager.cleanUp(); this._element.classList.remove(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS); } @@ -98,8 +105,10 @@ export class NavigateAwayPrompt { // allow the event to happen if the form was not touched by the // user (i.e. if the current FormData is equal to the initial FormData) // or the unload event was initiated by a form submit - if (!formDataHasChanged || this._unloadDueToSubmit) + if (!formDataHasChanged || this._unloadDueToSubmit) { + this.destroy(); //prevent propmt from triggering after the form was closed and the user navigates away return; + } // cancel the unload event. This is the standard to force the prompt to appear. event.preventDefault(); From db30fa642333780e0d8e7ac8f6a0d9859b80729f Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 13 Jul 2021 12:41:44 +0200 Subject: [PATCH 052/143] chore(reactive-submit-button): implemented destroy --- frontend/src/utils/form/reactive-submit-button.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/utils/form/reactive-submit-button.js b/frontend/src/utils/form/reactive-submit-button.js index e46eed77e..c5bc3c642 100644 --- a/frontend/src/utils/form/reactive-submit-button.js +++ b/frontend/src/utils/form/reactive-submit-button.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS = 'reactive-submit-button--initialized'; @@ -12,12 +13,15 @@ export class ReactiveSubmitButton { _requiredInputs; _submitButton; + _eventManager; + constructor(element) { if (!element) { throw new Error('Reactive Submit Button utility cannot be setup without an element!'); } this._element = element; + this._eventManager = new EventManager(); if (this._element.classList.contains(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS)) { return false; @@ -51,16 +55,18 @@ export class ReactiveSubmitButton { } destroy() { - // TODO + this._eventManager.removeAllEventListenersFromUtil(); + this._element.classList.remove(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS); } setupInputs() { this._requiredInputs.forEach((el) => { const checkbox = el.getAttribute('type') === 'checkbox'; - const eventType = checkbox ? 'change' : 'input'; - el.addEventListener(eventType, () => { + const eventType = checkbox ? EVENT_TYPE.CHANGE : EVENT_TYPE.INPUT; + const valEvent = new EventWrapper(eventType,(() => { this.updateButtonState(); - }); + }).bind(this), el ); + this._eventManager.registerNewListener(valEvent); }); } From abe84156d508dca8fce549b24d5902d24afc0dbf Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 16 Jul 2021 14:00:32 +0200 Subject: [PATCH 053/143] fix: prompt not shwowing up after submit/close --- frontend/src/utils/async-form/async-form.js | 2 +- .../src/utils/form/navigate-away-prompt.js | 12 ++++-- frontend/src/utils/modal/modal.js | 41 ++++++++++++++----- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/frontend/src/utils/async-form/async-form.js b/frontend/src/utils/async-form/async-form.js index 19e147be8..17f2c0810 100644 --- a/frontend/src/utils/async-form/async-form.js +++ b/frontend/src/utils/async-form/async-form.js @@ -100,8 +100,8 @@ export class AsyncForm { ).catch(() => { const failureMessage = this._app.i18n.get('asyncFormFailure'); this._processResponse({ content: failureMessage }); - this._element.classList.remove(ASYNC_FORM_LOADING_CLASS); }); + this._app.utilRegistry.destroyAll(this._element); } } diff --git a/frontend/src/utils/form/navigate-away-prompt.js b/frontend/src/utils/form/navigate-away-prompt.js index 9c6a57eee..fd671bc81 100644 --- a/frontend/src/utils/form/navigate-away-prompt.js +++ b/frontend/src/utils/form/navigate-away-prompt.js @@ -27,18 +27,20 @@ const NAVIGATE_AWAY_PROMPT_UTIL_OPTOUT = '[uw-no-navigate-away-prompt]'; export class NavigateAwayPrompt { _element; + _app; _eventManager; _initFormData; _unloadDueToSubmit = false; - constructor(element) { + constructor(element, app) { if (!element) { throw new Error('Navigate Away Prompt utility needs to be passed an element!'); } this._element = element; + this._app = app; this._eventManager = new EventManager(); if (this._element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) { @@ -105,8 +107,12 @@ export class NavigateAwayPrompt { // allow the event to happen if the form was not touched by the // user (i.e. if the current FormData is equal to the initial FormData) // or the unload event was initiated by a form submit - if (!formDataHasChanged || this._unloadDueToSubmit) { - this.destroy(); //prevent propmt from triggering after the form was closed and the user navigates away + if (!formDataHasChanged) + return; + + + if(this._unloadDueToSubmit) { + this._app.utilRegistry.destroyAll(this._element); return; } diff --git a/frontend/src/utils/modal/modal.js b/frontend/src/utils/modal/modal.js index c67b13ac7..f45770d10 100644 --- a/frontend/src/utils/modal/modal.js +++ b/frontend/src/utils/modal/modal.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './modal.sass'; const MODAL_HEADERS = { @@ -28,12 +29,16 @@ const MODALS_WRAPPER_OPEN_CLASS = 'modals-wrapper--open'; }) export class Modal { + _eventManager + _element; _app; _modalsWrapper; _modalOverlay; _modalUrl; + _triggerElement; + _closerElement; constructor(element, app) { if (!element) { @@ -42,6 +47,7 @@ export class Modal { this._element = element; this._app = app; + this._eventManager = new EventManager(); if (this._element.classList.contains(MODAL_INITIALIZED_CLASS)) { return false; @@ -66,7 +72,16 @@ export class Modal { } destroy() { - // TODO + this._eventManager.cleanUp(); + if (this._closerElement !== undefined) + this._closerElement.remove(); + if(this._triggerElement !== undefined) + this._triggerElement.classList.remove(MODAL_TRIGGER_CLASS); + if(this._modalsWrapper !== undefined) + this._modalsWrapper.remove(); + if(this._modalOverlay !== undefined) + this._modalOverlay.remove(); + this._element.classList.remove(MODAL_INITIALIZED_CLASS, MODAL_CLASS); } _ensureModalWrapper() { @@ -92,23 +107,26 @@ export class Modal { if (!triggerSelector.startsWith('#')) { triggerSelector = '#' + triggerSelector; } - const triggerElement = document.querySelector(triggerSelector); + this._triggerElement = document.querySelector(triggerSelector); - if (!triggerElement) { + if (!this._triggerElement) { throw new Error('Trigger element for Modal not found: "' + triggerSelector + '"'); } - triggerElement.classList.add(MODAL_TRIGGER_CLASS); - triggerElement.addEventListener('click', this._onTriggerClicked, false); - this._modalUrl = triggerElement.getAttribute('href'); + this._triggerElement.classList.add(MODAL_TRIGGER_CLASS); + const triggerEvent = new EventWrapper(EVENT_TYPE.CLICK, this._onTriggerClicked.bind(this), this._triggerElement, false); + this._eventManager.registerNewListener(triggerEvent); + this._modalUrl = this._triggerElement.getAttribute('href'); } _setupCloser() { - const closerElement = document.createElement('div'); - this._element.insertBefore(closerElement, null); - closerElement.classList.add(MODAL_CLOSER_CLASS); - closerElement.addEventListener('click', this._onCloseClicked, false); - this._modalOverlay.addEventListener('click', this._onCloseClicked, false); + this._closerElement = document.createElement('div'); + this._element.insertBefore(this._closerElement, null); + this._closerElement.classList.add(MODAL_CLOSER_CLASS); + const closerElEvent = new EventWrapper(EVENT_TYPE.CLICK, this._onCloseClicked.bind(this), this._closerElement, false); + this._eventManager.registerNewListener(closerElEvent); + const overlayClose = new EventWrapper(EVENT_TYPE.CLICK, this._onCloseClicked.bind(this), this._modalOverlay, false); + this._eventManager.registerNewListener(overlayClose); } _onTriggerClicked = (event) => { @@ -119,6 +137,7 @@ export class Modal { _onCloseClicked = (event) => { event.preventDefault(); this._close(); + this._app.utilRegistry.destroyAll(this._element); } _onKeyUp = (event) => { From 9d5b8bc60ad6c692ccf68282f8add9640021cde4 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 16 Jul 2021 14:57:42 +0200 Subject: [PATCH 054/143] chore(hide-columns): implemented destroy --- .../src/lib/event-manager/event-manager.js | 1 + .../src/utils/hide-columns/hide-columns.js | 43 +++++++++++++------ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index 35a5075e6..499ede4f9 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -5,6 +5,7 @@ export const EVENT_TYPE = { INVALID : 'invalid', CHANGE : 'change', MOUSE_OVER : 'mouseover', + MOUSE_OUT : 'mouseout', SUBMIT : 'submit', INPUT : 'input', FOCUS_OUT : 'focusout', diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 7661ac92b..abfa37eb6 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -1,5 +1,7 @@ import { Utility } from '../../core/utility'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; + import './hide-columns.sass'; import { TableIndices } from '../../lib/table/table'; @@ -29,6 +31,7 @@ const HIDE_COLUMNS_INITIALIZED = 'uw-hide-columns--initialized'; export class HideColumns { _storageManager = new StorageManager('HIDE_COLUMNS', '1.1.0', { location: LOCATION.LOCAL }); + _eventManager; _element; _elementWrapper; @@ -36,8 +39,6 @@ export class HideColumns { _autoHide; - _mutationObserver; - _tableIndices; headerToHider = new Map(); @@ -62,6 +63,7 @@ export class HideColumns { return false; this._element = element; + this._eventManager = new EventManager(); this._tableIndices = new TableIndices(this._element); @@ -82,12 +84,18 @@ export class HideColumns { [...this._element.querySelectorAll('th')].filter(th => !th.hasAttribute(HIDE_COLUMNS_NO_HIDE)).forEach(th => this.setupHideButton(th)); - this._mutationObserver = new MutationObserver(this._tableMutated.bind(this)); - this._mutationObserver.observe(this._element, { childList: true, subtree: true }); + this._eventManager.registerNewMutationObserver(this._tableMutated.bind(this), this._element, { childList: true, subtree: true }); this._element.classList.add(HIDE_COLUMNS_INITIALIZED); } + destroy() { + this._eventManager.cleanUp(); + this._storageManager.clear(); + this._tableUtilContainer.remove(); + this._element.classList.remove(HIDE_COLUMNS_INITIALIZED); + } + setupHideButton(th) { const preHidden = this.isHiddenTH(th); @@ -104,34 +112,41 @@ export class HideColumns { this.addHeaderHider(th, hider); - th.addEventListener('mouseover', () => { + const mouseOverEvent = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); - }); - th.addEventListener('mouseout', () => { + }).bind(this), th); + this._eventManager.registerNewListener(mouseOverEvent); + + const mouseOutEvent = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } - }); + }).bind(this), th); + this._eventManager.registerNewListener(mouseOutEvent); - hider.addEventListener('click', (event) => { + const hideClickEvent = new EventWrapper(EVENT_TYPE.CLICK, ((event) => { event.preventDefault(); event.stopPropagation(); this.switchColumnDisplay(th); // this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); - }); + }).bind(this)); + this._eventManager.registerNewListener(hideClickEvent); - hider.addEventListener('mouseover', () => { + const mouseOverHider = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); const currentlyHidden = this.hiderStatus(th); this.updateHiderIcon(hider, !currentlyHidden); - }); - hider.addEventListener('mouseout', () => { + }).bind(this), hider); + this._eventManager.registerNewListener(mouseOverHider); + + const mouseOutHider = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } const currentlyHidden = this.hiderStatus(th); this.updateHiderIcon(hider, currentlyHidden); - }); + }).bind(this), hider); + this._eventManger.registerNewListener(mouseOutHider); new ResizeObserver(() => { this.repositionHider(hider); }).observe(th); From 50a3ac1790f26c1e6f983d3687352d7811173110 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 20 Jul 2021 11:05:15 +0200 Subject: [PATCH 055/143] fix(hide-colums): small fix --- frontend/src/utils/hide-columns/hide-columns.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index abfa37eb6..bb2487c38 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -129,7 +129,7 @@ export class HideColumns { event.stopPropagation(); this.switchColumnDisplay(th); // this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); - }).bind(this)); + }).bind(this), hider); this._eventManager.registerNewListener(hideClickEvent); const mouseOverHider = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { @@ -146,7 +146,7 @@ export class HideColumns { const currentlyHidden = this.hiderStatus(th); this.updateHiderIcon(hider, currentlyHidden); }).bind(this), hider); - this._eventManger.registerNewListener(mouseOutHider); + this._eventManager.registerNewListener(mouseOutHider); new ResizeObserver(() => { this.repositionHider(hider); }).observe(th); From dedbab689f1f9793b9cc3ebac160733d2f79e770 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 20 Jul 2021 11:05:55 +0200 Subject: [PATCH 056/143] chore(checkbox): implemented destroy --- frontend/src/utils/inputs/checkbox.js | 34 +++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/frontend/src/utils/inputs/checkbox.js b/frontend/src/utils/inputs/checkbox.js index 9b1b541ff..8c96d201d 100644 --- a/frontend/src/utils/inputs/checkbox.js +++ b/frontend/src/utils/inputs/checkbox.js @@ -9,43 +9,53 @@ const CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized'; selector: 'input[type="checkbox"]:not([uw-no-checkbox]), input[type="radio"]:not([uw-no-radiobox])', }) export class Checkbox { + + _element; + _wrapperEl; + constructor(element) { if (!element) { throw new Error('Checkbox utility cannot be setup without an element!'); } + this._element = element; - const isRadio = element.type === 'radio'; + const isRadio = this._element.type === 'radio'; const box_class = isRadio ? RADIOBOX_CLASS : CHECKBOX_CLASS; - if (isRadio && element.closest('.radio-group')) { + if (isRadio && this._element.closest('.radio-group')) { // Don't initialize radiobox, if radio is part of a group return false; } - if (element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) { + if (this._element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) { // throw new Error('Checkbox utility already initialized!'); return false; } - if (element.parentElement.classList.contains(box_class)) { + if (this._element.parentElement.classList.contains(box_class)) { // throw new Error('Checkbox element\'s wrapper already has class '' + box_class + ''!'); return false; } - const siblingEl = element.nextSibling; - const parentEl = element.parentElement; + const siblingEl = this._element.nextSibling; + const parentEl = this._element.parentElement; - const wrapperEl = document.createElement('div'); - wrapperEl.classList.add(box_class); + this._wrapperEl = document.createElement('div'); + this._wrapperEl.classList.add(box_class); const labelEl = document.createElement('label'); labelEl.setAttribute('for', element.id); - wrapperEl.appendChild(element); - wrapperEl.appendChild(labelEl); + this._wrapperEl.appendChild(element); + this._wrapperEl.appendChild(labelEl); - parentEl.insertBefore(wrapperEl, siblingEl); + parentEl.insertBefore(this._wrapperEl, siblingEl); - element.classList.add(CHECKBOX_INITIALIZED_CLASS); + this._element.classList.add(CHECKBOX_INITIALIZED_CLASS); + } + + destroy() { + this._wrapperEl.remove(); + this._element.classList.remove(CHECKBOX_INITIALIZED_CLASS); } } From 8cb3a41fcb96ce381a9c2e59001a7cedf67eb063 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 20 Jul 2021 11:10:21 +0200 Subject: [PATCH 057/143] chore(file-input): implemented destroy --- frontend/src/utils/inputs/file-input.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/utils/inputs/file-input.js b/frontend/src/utils/inputs/file-input.js index e84d2ce26..6145a4970 100644 --- a/frontend/src/utils/inputs/file-input.js +++ b/frontend/src/utils/inputs/file-input.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './file-input.sass'; const FILE_INPUT_CLASS = 'file-input'; @@ -19,6 +20,8 @@ export class FileInput { _fileList; _label; + _eventManager; + constructor(element, app) { if (!element) { throw new Error('FileInput utility cannot be setup without an element!'); @@ -27,6 +30,8 @@ export class FileInput { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (this._element.classList.contains(FILE_INPUT_INITIALIZED_CLASS)) { throw new Error('FileInput utility already initialized!'); } @@ -40,11 +45,11 @@ export class FileInput { this._label = this._createFileLabel(); this._updateLabel(); - // add change listener - this._element.addEventListener('change', () => { + const changeInputEv = new EventWrapper(EVENT_TYPE.CHANGE,(() => { this._updateLabel(); this._renderFileList(); - }); + }).bind(this), this._element ); + this._eventManager.registerNewListener(changeInputEv); // add util class for styling and mark as initialized this._element.classList.add(FILE_INPUT_CLASS); @@ -52,7 +57,9 @@ export class FileInput { } destroy() { - // TODO + this._fileList.remove(); + this._label.remove(); + this._element.classList.remove(FILE_INPUT_INITIALIZED_CLASS); } _renderFileList() { From 1153ba8711c141c5b49ecf352d6a1ee2e6f3eb57 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 20 Jul 2021 11:17:16 +0200 Subject: [PATCH 058/143] chore(file-max-size): implemented destroy --- frontend/src/utils/inputs/file-input.js | 1 + frontend/src/utils/inputs/file-max-size.js | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/inputs/file-input.js b/frontend/src/utils/inputs/file-input.js index 6145a4970..300a15fa4 100644 --- a/frontend/src/utils/inputs/file-input.js +++ b/frontend/src/utils/inputs/file-input.js @@ -57,6 +57,7 @@ export class FileInput { } destroy() { + this._eventManager.cleanUp(); this._fileList.remove(); this._label.remove(); this._element.classList.remove(FILE_INPUT_INITIALIZED_CLASS); diff --git a/frontend/src/utils/inputs/file-max-size.js b/frontend/src/utils/inputs/file-max-size.js index 653cca287..d0b8e75f5 100644 --- a/frontend/src/utils/inputs/file-max-size.js +++ b/frontend/src/utils/inputs/file-max-size.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const FILE_MAX_SIZE_INITIALIZED_CLASS = 'file-max-size--initialized'; @@ -9,6 +10,8 @@ export class FileMaxSize { _element; _app; + _eventManager; + constructor(element, app) { if (!element) throw new Error('FileMaxSize utility cannot be setup without an element!'); @@ -16,6 +19,8 @@ export class FileMaxSize { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (this._element.classList.contains(FILE_MAX_SIZE_INITIALIZED_CLASS)) { throw new Error('FileMaxSize utility already initialized!'); } @@ -24,7 +29,13 @@ export class FileMaxSize { } start() { - this._element.addEventListener('change', this._change.bind(this)); + const changeEv = new EventWrapper(EVENT_TYPE.CHANGE, this._change.bind(this), this._element); + this._eventManager.registerNewListener(changeEv); + } + + destroy() { + this._eventManager.cleanUp(); + this._element.classList.remove(FILE_MAX_SIZE_INITIALIZED_CLASS); } _change() { From b1c662da88df5853c3e047bbce4b4b6e93429154 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 21 Jul 2021 10:54:45 +0200 Subject: [PATCH 059/143] chore(password): implemented destroy --- frontend/src/utils/inputs/password.js | 40 +++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/frontend/src/utils/inputs/password.js b/frontend/src/utils/inputs/password.js index 2bb750802..3f5fe3b8b 100644 --- a/frontend/src/utils/inputs/password.js +++ b/frontend/src/utils/inputs/password.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const PASSWORD_INITIALIZED_CLASS = 'password-input--initialized'; @@ -9,6 +10,9 @@ export class Password { _element; _iconEl; _toggleContainerEl; + _wrapperEl; + + _eventManager; constructor(element) { if (!element) @@ -18,25 +22,26 @@ export class Password { return false; this._element = element; + this._eventManager = new EventManager(); this._element.classList.add('password-input__input'); const siblingEl = this._element.nextSibling; const parentEl = this._element.parentElement; - const wrapperEl = document.createElement('div'); - wrapperEl.classList.add('password-input__wrapper'); - wrapperEl.appendChild(this._element); + this._wrapperEl = document.createElement('div'); + this._wrapperEl.classList.add('password-input__wrapper'); + this._wrapperEl.appendChild(this._element); this._toggleContainerEl = document.createElement('div'); this._toggleContainerEl.classList.add('password-input__toggle'); - wrapperEl.appendChild(this._toggleContainerEl); + this._wrapperEl.appendChild(this._toggleContainerEl); this._iconEl = document.createElement('i'); this._iconEl.classList.add('fas', 'fa-fw'); this._toggleContainerEl.appendChild(this._iconEl); - parentEl.insertBefore(wrapperEl, siblingEl); + parentEl.insertBefore(this._wrapperEl, siblingEl); this._element.classList.add(PASSWORD_INITIALIZED_CLASS); } @@ -44,17 +49,30 @@ export class Password { start() { this.updateVisibleIcon(this.isVisible()); - this._toggleContainerEl.addEventListener('mouseover', () => { + const mouseOverEv = new EventWrapper(EVENT_TYPE.MOUS_OVER, (() => { this.updateVisibleIcon(!this.isVisible()); - }); - this._toggleContainerEl.addEventListener('mouseout', () => { + }).bind(this), this._toggleContainerEl); + + const mouseOutEv = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => { this.updateVisibleIcon(this.isVisible()); - }); - this._toggleContainerEl.addEventListener('click', (event) => { + }).bind(this), this._toggleContainerEl); + + const clickEv = new EventWrapper(EVENT_TYPE.CLICK,((event) => { event.preventDefault(); event.stopPropagation(); this.setVisible(!this.isVisible()); - }); + }).bind(this), this._toggleContainerEl ); + + this._eventManager.registerListeners([mouseOverEv, mouseOutEv, clickEv]); + } + + destroy() { + this._iconEl.remove(); + this._toggleContainerEl.remove(); + this._wrapperEl.remove(); + this._iconEl.remove(); + + this._element.classList.remove(PASSWORD_INITIALIZED_CLASS); } isVisible() { From da8894a708b1c3b43d7462a27750c96431006fcc Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 21 Jul 2021 12:15:56 +0200 Subject: [PATCH 060/143] chore(radio): implemented destroy --- frontend/src/utils/inputs/radio.js | 38 ++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/frontend/src/utils/inputs/radio.js b/frontend/src/utils/inputs/radio.js index 38a3f0f2f..311f3fb53 100644 --- a/frontend/src/utils/inputs/radio.js +++ b/frontend/src/utils/inputs/radio.js @@ -9,39 +9,51 @@ const RADIO_INITIALIZED_CLASS = 'radio--initialized'; }) export class Radio { + _element; + _wrapperEl; + _labelEl; + constructor(element) { if (!element) { throw new Error('Radio utility cannot be setup without an element!'); } - if (element.closest('.radio-group')) { + this._element = element; + + if (this._element.closest('.radio-group')) { return false; } - if (element.classList.contains(RADIO_INITIALIZED_CLASS)) { + if (this._element.classList.contains(RADIO_INITIALIZED_CLASS)) { // throw new Error('Radio utility already initialized!'); return false; } - if (element.parentElement.classList.contains(RADIO_CLASS)) { + if (this._element.parentElement.classList.contains(RADIO_CLASS)) { // throw new Error('Radio element\'s wrapper already has class '' + RADIO_CLASS + ''!'); return false; } - const siblingEl = element.nextSibling; - const parentEl = element.parentElement; + const siblingEl = this._element.nextSibling; + const parentEl = this._element.parentElement; - const wrapperEl = document.createElement('div'); - wrapperEl.classList.add(RADIO_CLASS); + this._wrapperEl = document.createElement('div'); + this._wrapperEl.classList.add(RADIO_CLASS); - const labelEl = document.createElement('label'); - labelEl.setAttribute('for', element.id); + this._labelEl = document.createElement('label'); + this._labelEl.setAttribute('for', this._element.id); - wrapperEl.appendChild(element); - wrapperEl.appendChild(labelEl); + this._wrapperEl.appendChild(this._element); + this._wrapperEl.appendChild(this._labelEl); - parentEl.insertBefore(wrapperEl, siblingEl); + parentEl.insertBefore(this._wrapperEl, siblingEl); - element.classList.add(RADIO_INITIALIZED_CLASS); + this._element.classList.add(RADIO_INITIALIZED_CLASS); + } + + destroy() { + this._labelEl.remove(); + this._wrapperEl.remove(); + this._element.classList.remove(RADIO_INITIALIZED_CLASS); } } From 1a8fb230441f73b9b1fe593f4df1005a06d9628e Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 21 Jul 2021 12:17:41 +0200 Subject: [PATCH 061/143] feat(event-manager): added method to register a list of listeners --- frontend/src/lib/event-manager/event-manager.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index 499ede4f9..03a798308 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -31,6 +31,10 @@ export class EventManager { this._registeredListeners.push(eventWrapper); } + registerListeners(eventWrappers) { + eventWrappers.forEach((eventWrapper) => this.registerNewListener(eventWrapper)); + } + registerNewMutationObserver(callback, domNode, config) { let observer = new MutationObserver(callback); observer.observe(domNode, config); From 1f978e65a82a91fb728a7ee2970a4fd9e6beb521 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 21 Jul 2021 12:30:23 +0200 Subject: [PATCH 062/143] fix: smaller fixes and typos --- frontend/src/utils/check-all/check-all.js | 5 +++-- frontend/src/utils/inputs/checkbox.js | 3 ++- frontend/src/utils/inputs/password.js | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/check-all/check-all.js b/frontend/src/utils/check-all/check-all.js index df151bc62..7d051cddd 100644 --- a/frontend/src/utils/check-all/check-all.js +++ b/frontend/src/utils/check-all/check-all.js @@ -41,7 +41,7 @@ export class CheckAll { if (DEBUG_MODE > 0) console.log(this._columns); - this._findCheckboxColumns().forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId]))); + this._findCheckboxColumns().forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId], this._eventManager))); // mark initialized this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS); @@ -50,7 +50,8 @@ export class CheckAll { destroy() { this._eventManager.removeAllEventListenersFromUtil(); this._checkAllColumns.forEach((column) => { - column._checkAllCheckBox.remove(); + if (column._checkAllCheckBox !== undefined) + column._checkAllCheckBox.remove(); }); if(this._element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) diff --git a/frontend/src/utils/inputs/checkbox.js b/frontend/src/utils/inputs/checkbox.js index 8c96d201d..ebf721667 100644 --- a/frontend/src/utils/inputs/checkbox.js +++ b/frontend/src/utils/inputs/checkbox.js @@ -55,7 +55,8 @@ export class Checkbox { } destroy() { - this._wrapperEl.remove(); + if (this._wrapperEl !== undefined) + this._wrapperEl.remove(); this._element.classList.remove(CHECKBOX_INITIALIZED_CLASS); } } diff --git a/frontend/src/utils/inputs/password.js b/frontend/src/utils/inputs/password.js index 3f5fe3b8b..3793598ee 100644 --- a/frontend/src/utils/inputs/password.js +++ b/frontend/src/utils/inputs/password.js @@ -49,7 +49,7 @@ export class Password { start() { this.updateVisibleIcon(this.isVisible()); - const mouseOverEv = new EventWrapper(EVENT_TYPE.MOUS_OVER, (() => { + const mouseOverEv = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { this.updateVisibleIcon(!this.isVisible()); }).bind(this), this._toggleContainerEl); @@ -57,7 +57,7 @@ export class Password { this.updateVisibleIcon(this.isVisible()); }).bind(this), this._toggleContainerEl); - const clickEv = new EventWrapper(EVENT_TYPE.CLICK,((event) => { + const clickEv = new EventWrapper(EVENT_TYPE.CLICK, ((event) => { event.preventDefault(); event.stopPropagation(); this.setVisible(!this.isVisible()); From 74bb9fb548dfbb30ab367267acfec654c55f242d Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 26 Jul 2021 11:36:28 +0200 Subject: [PATCH 063/143] chore(mass-input): implemented destroy --- frontend/src/utils/mass-input/mass-input.js | 25 ++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/frontend/src/utils/mass-input/mass-input.js b/frontend/src/utils/mass-input/mass-input.js index aaa5f7d0c..184706b4d 100644 --- a/frontend/src/utils/mass-input/mass-input.js +++ b/frontend/src/utils/mass-input/mass-input.js @@ -2,6 +2,7 @@ import { Utility } from '../../core/utility'; import { Datepicker } from '../form/datepicker'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './mass-input.sass'; const MASS_INPUT_CELL_SELECTOR = '.massinput__cell'; @@ -29,6 +30,8 @@ export class MassInput { _changedAdd = new Array(); + _eventManager; + constructor(element, app) { if (!element) { throw new Error('Mass Input utility cannot be setup without an element!'); @@ -37,6 +40,8 @@ export class MassInput { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (global !== undefined) this._global = global; else if (window !== undefined) @@ -64,9 +69,10 @@ export class MassInput { buttons.forEach((button) => { this._setupSubmitButton(button); }); - - this._massInputForm.addEventListener('submit', this._massInputFormSubmitHandler.bind(this)); - this._massInputForm.addEventListener('keypress', this._keypressHandler.bind(this)); + + const submitEv = new EventWrapper(EVENT_TYPE.SUBMIT, this._massInputFormSubmitHandler.bind(this), this._massInputForm); + const keyPressEv = new EventWrapper(EVENT_TYPE.KEYDOWN, this._keypressHandler.bind(this), this.massInputForm); + this._eventManager.registerListeners([submitEv, keyPressEv]); Array.from(this._element.querySelectorAll(MASS_INPUT_ADD_CELL_SELECTOR)).forEach(this._setupChangedHandlers.bind(this)); @@ -76,14 +82,16 @@ export class MassInput { destroy() { this._reset(); + this._eventManager.cleanUp(); + this._element.classList.remove(MASS_INPUT_INITIALIZED_CLASS); } _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); }); + const changeEv = new EventWrapper(EVENT_TYPE.CHANGE, (() => { this._changedAdd.push(addCell); }).bind(this), inputElem); + this._eventManager.registerNewListener(changeEv); }); } @@ -207,13 +215,13 @@ export class MassInput { _setupSubmitButton(button) { button.setAttribute('type', 'button'); button.classList.add(MASS_INPUT_SUBMIT_BUTTON_CLASS); - button.addEventListener('click', this._massInputFormSubmitHandler); + const buttonClickEv = new EventWrapper(EVENT_TYPE.CLICK, this._massInputFormSubmitHandler.bind(this), button); + this._eventManager.registerNewListener(buttonClickEv); } _resetSubmitButton(button) { button.setAttribute('type', 'submit'); button.classList.remove(MASS_INPUT_SUBMIT_BUTTON_CLASS); - button.removeEventListener('click', this._massInputFormSubmitHandler); } _processResponse(responseElement) { @@ -268,9 +276,6 @@ export class MassInput { } _reset() { - this._element.classList.remove(MASS_INPUT_INITIALIZED_CLASS); - this._massInputForm.removeEventListener('submit', this._massInputFormSubmitHandler); - this._massInputForm.removeEventListener('keypress', this._keypressHandler); const buttons = this._getMassInputSubmitButtons(); buttons.forEach((button) => { From 03b6e199f1add7805202d2b3e7bbc5a7811d8e8c Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 26 Jul 2021 18:11:28 +0200 Subject: [PATCH 064/143] chore(navbar): implemented destroy --- frontend/src/utils/navbar/navbar.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/navbar/navbar.js b/frontend/src/utils/navbar/navbar.js index f31ba77bd..08c11428c 100644 --- a/frontend/src/utils/navbar/navbar.js +++ b/frontend/src/utils/navbar/navbar.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './navbar.sass'; import * as throttle from 'lodash.throttle'; @@ -18,6 +19,8 @@ export class NavHeaderContainerUtil { _throttleUpdateWasOpen; + _eventManager; + constructor(element) { if (!element) { throw new Error('Navbar Header Container utility needs to be passed an element!'); @@ -29,6 +32,9 @@ export class NavHeaderContainerUtil { this._element = element; this.radioButton = document.getElementById(`${this._element.id}-radio`); + + this._eventManager = new EventManager(); + if (!this.radioButton) { throw new Error('Navbar Header Container utility could not find associated radio button!'); } @@ -58,8 +64,9 @@ export class NavHeaderContainerUtil { if (!this.container) return; - window.addEventListener('click', this.clickHandler.bind(this)); - this.radioButton.addEventListener('change', this.throttleUpdateWasOpen.bind(this)); + const clickEv = new EventWrapper(EVENT_TYPE.CLICK, this.clickHandler.bind(this), window); + const changeEv = new EventWrapper(EVENT_TYPE.CHANGE, this.throttleUpdateWasOpen.bind(this), this.radioButton); + this._eventManager.registerListeners([clickEv, changeEv]); } clickHandler() { @@ -81,7 +88,10 @@ export class NavHeaderContainerUtil { this.wasOpen = this.isOpen(); } - destroy() { /* TODO */ } + destroy() { + this._eventManager.cleanUp(); + this._element.classList.remove(HEADER_CONTAINER_INITIALIZED_CLASS); + } } From d18f822ca5f622857b153d7ba35c2788765c7886 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 26 Jul 2021 18:20:46 +0200 Subject: [PATCH 065/143] chore(show-hide): implemented destroy --- .../src/lib/event-manager/event-manager.js | 1 + frontend/src/utils/show-hide/show-hide.js | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index 03a798308..edc97c91f 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -10,6 +10,7 @@ export const EVENT_TYPE = { INPUT : 'input', FOCUS_OUT : 'focusout', BEFOREUNLOAD : 'beforeunload', + HASH_CHANGE : 'hashchange', }; diff --git a/frontend/src/utils/show-hide/show-hide.js b/frontend/src/utils/show-hide/show-hide.js index 3419ee1f4..e7f7c3315 100644 --- a/frontend/src/utils/show-hide/show-hide.js +++ b/frontend/src/utils/show-hide/show-hide.js @@ -1,5 +1,6 @@ import { Utility } from '../../core/utility'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './show-hide.sass'; const SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE'; @@ -16,6 +17,7 @@ export class ShowHide { _showHideId; _element; + _eventManager; _storageManager = new StorageManager(SHOW_HIDE_LOCAL_STORAGE_KEY, '1.0.0', { location: LOCATION.LOCAL }); constructor(element) { @@ -24,13 +26,15 @@ export class ShowHide { } this._element = element; + this._eventManager = new EventManager(); if (this._element.classList.contains(SHOW_HIDE_INITIALIZED_CLASS)) { return false; } // register click listener - this._addClickListener(); + const clickEv = new EventWrapper(EVENT_TYPE.CLICK, this._clickHandler.bind(this), this._element); + this._eventManager.registerNewListener(clickEv); // param showHideId if (this._element.dataset.showHideId) { @@ -58,17 +62,19 @@ export class ShowHide { } this._checkHash(); - - window.addEventListener('hashchange', this._checkHash.bind(this)); + const hashChangeEv = new EventWrapper(EVENT_TYPE.HASH_CHANGE, this._checkHash.bind(this), window); + this._eventManager.registerNewListener(hashChangeEv); // mark as initialized this._element.classList.add(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS); } - destroy() {} - - _addClickListener() { - this._element.addEventListener('click', this._clickHandler.bind(this)); + destroy() { + this._eventManager.cleanUp(); + this._storageManager.clear(); + if(this._element.parentElement.contains(SHOW_HIDE_COLLAPSED_CLASS)) + this._element.parentElement.classList.remove(SHOW_HIDE_COLLAPSED_CLASS); + this._element.classList.remove(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS); } _show() { From 2796c940f0b3762f03f9cad9562c2847da333c85 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 26 Jul 2021 18:43:03 +0200 Subject: [PATCH 066/143] chore(pageactions): implemented destroy --- frontend/src/utils/pageactions/pageactions.js | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/frontend/src/utils/pageactions/pageactions.js b/frontend/src/utils/pageactions/pageactions.js index 7c2334b6e..870065a4e 100644 --- a/frontend/src/utils/pageactions/pageactions.js +++ b/frontend/src/utils/pageactions/pageactions.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './pageactions.sass'; import * as throttle from 'lodash.throttle'; @@ -17,9 +18,12 @@ export class PageActionSecondaryUtil { closeButton; container; wasOpen; + _closer; _throttleUpdateWasOpen; + _eventManager; + constructor(element) { if (!element) { throw new Error('Pageaction Secondary utility needs to be passed an element!'); @@ -31,6 +35,8 @@ export class PageActionSecondaryUtil { this._element = element; + this._eventManager = new EventManager(); + const childContainer = this._element.querySelector('.pagenav-item__children'); if (!childContainer) { @@ -43,7 +49,7 @@ export class PageActionSecondaryUtil { const links = Array.from(this._element.querySelectorAll('.pagenav-item__link')).filter(l => !childContainer.contains(l)); if (!links || Array.from(links).length !== 1) { - throw new Error('Pageaction Secondary utility could not find associated link!'); + throw new Error('Pageaction Secondary utility could not find associated link!'); } this.navIdent = links[0].id; } @@ -71,9 +77,9 @@ export class PageActionSecondaryUtil { throw new Error('Pageaction Secondary utility could not find associated container!'); } - const closer = this._element.querySelector('.pagenav-item__close-label'); - if (closer) { - closer.classList.add('pagenav-item__close-label--hidden'); + this._closer = this._element.querySelector('.pagenav-item__close-label'); + if (this._closer) { + this._closer.classList.add('pagenav-item__close-label--hidden'); } this.updateWasOpen(); @@ -85,12 +91,12 @@ export class PageActionSecondaryUtil { start() { if (!this.container) return; - - window.addEventListener('click', this.clickHandler.bind(this)); - this.radioButton.addEventListener('change', this.throttleUpdateWasOpen.bind(this)); + const windowClickEv = new EventWrapper(EVENT_TYPE.CLICK, ((event) => this.clickHandler(event)).bind(this), window); + const radioButtonChangeEv = new EventWrapper(EVENT_TYPE.CHANGE, this.throttleUpdateWasOpen.bind(this), this.radioButton); + this._eventManager.registerListeners([windowClickEv, radioButtonChangeEv]); } - clickHandler() { + clickHandler(event) { if (!this.container.contains(event.target) && window.document.contains(event.target) && this.wasOpen) { this.close(); } @@ -109,7 +115,12 @@ export class PageActionSecondaryUtil { this.wasOpen = this.isOpen(); } - destroy() { /* TODO */ } + destroy() { + this._eventManager.cleanUp(); + if(this._closer && this._closer.classList.contains()) + this._closer.classList.remove('pagenav-item__close-label--hidden'); + this._element.classList.remove(PAGEACTION_SECONDARY_INITIALIZED_CLASS); + } } From 4156db3abba1bb10608c50f7ae1c19de7f410da0 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 26 Jul 2021 18:47:18 +0200 Subject: [PATCH 067/143] chore(sort-table): implemented destroy --- frontend/src/utils/sort-table/sort-table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/sort-table/sort-table.js b/frontend/src/utils/sort-table/sort-table.js index 3c43a9ee2..639664685 100644 --- a/frontend/src/utils/sort-table/sort-table.js +++ b/frontend/src/utils/sort-table/sort-table.js @@ -21,7 +21,7 @@ export class SortTable { } destroy() { - console.log('TBD destroy SortTable'); + this._storageManager.clear(); } } From 01d9ce39805ec548f46c205c156a310f2a01359c Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 26 Jul 2021 19:24:22 +0200 Subject: [PATCH 068/143] chore(tooltips): implemented destroy --- frontend/src/utils/tooltips/tooltips.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/tooltips/tooltips.js b/frontend/src/utils/tooltips/tooltips.js index 12d628f35..c520af204 100644 --- a/frontend/src/utils/tooltips/tooltips.js +++ b/frontend/src/utils/tooltips/tooltips.js @@ -1,6 +1,7 @@ import { Utility } from '../../core/utility'; import './tooltips.sass'; import { MovementObserver } from '../../lib/movement-observer/movement-observer'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const TOOLTIP_CLASS = 'tooltip'; const TOOLTIP_INITIALIZED_CLASS = 'tooltip--initialized'; @@ -17,6 +18,7 @@ export class Tooltip { _content; _movementObserver; + _eventManager; _openedPersistent = false; @@ -45,16 +47,19 @@ export class Tooltip { this._element = element; this._handle = element.querySelector('.tooltip__handle') || element; + this._eventManager = new EventManager(); + this._movementObserver = new MovementObserver(this._handle, { leadingCallback: this.close.bind(this) }); element.classList.add(TOOLTIP_INITIALIZED_CLASS); } start() { - this._element.addEventListener('mouseover', () => { this.open(false); }); - this._element.addEventListener('mouseout', this._leave.bind(this)); - this._content.addEventListener('mouseout', this._leave.bind(this)); - this._element.addEventListener('click', this._click.bind(this)); + const mouseOverEv = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { this.open(false); }).bind(this), this._element); + const mouseOutEv = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (this._leave.bind(this)).bind(this), this._element); + const contentMouseOut = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (this._leave.bind(this)).bind(this), this._content); + const clickEv = new EventWrapper(EVENT_TYPE.CLICK, this._click.bind(this), this._element); + this.registerListeners([mouseOverEv, mouseOutEv, contentMouseOut, clickEv]); } open(persistent) { @@ -183,5 +188,15 @@ export class Tooltip { } - destroy() {} + destroy() { + this._eventManager.cleanUp(); + if(this._element.classList.contains(TOOLTIP_OPEN_CLASS)) + this._element.classList.remove(TOOLTIP_OPEN_CLASS); + if(this._element.classList.contains('tooltip--right')) + this._element.classList.remove('tooltip--right'); + if(this._element.classList.contains('tooltip--bottom')) + this._element.classList.remove('tooltip--bottom'); + this._movementObserver.unobserve(); + this._element.classList.remove(TOOLTIP_INITIALIZED_CLASS); + } }; From 6320cd927a84445f056dc782fb440d276cb26009 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Thu, 29 Jul 2021 16:51:40 +0200 Subject: [PATCH 069/143] fix: fixed a few minor issues --- frontend/src/services/util-registry/util-registry.js | 6 +++--- frontend/src/utils/alerts/alerts.js | 6 +++--- frontend/src/utils/mass-input/mass-input.js | 2 +- frontend/src/utils/pageactions/pageactions.js | 2 +- frontend/src/utils/show-hide/show-hide.js | 4 ++-- frontend/src/utils/tooltips/tooltips.js | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index b8dea6ec3..67cc09702 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -101,9 +101,9 @@ export class UtilRegistry { let utilsInScope = this._getUtilInstancesWithinScope(scope); utilsInScope.forEach((util) => { - //if(DEBUG_MODE > 2) { - console.log('Destroying Util: ', {util}); - //}# + if(DEBUG_MODE > 2) { + console.log('Destroying Util: ', {util}); + } let utilIndex = this._activeUtilInstancesWrapped.indexOf(util); util.destroy(); this._activeUtilInstancesWrapped.splice(utilIndex, 1); diff --git a/frontend/src/utils/alerts/alerts.js b/frontend/src/utils/alerts/alerts.js index 4f84d8aac..8f7877e68 100644 --- a/frontend/src/utils/alerts/alerts.js +++ b/frontend/src/utils/alerts/alerts.js @@ -103,9 +103,9 @@ export class Alerts { } const closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS); - const closeAlertEvent = new EventWrapper(EVENT_TYPE.CLICK, () => { - this._toggleAlert(alertElement).bind(this); - }, closeEl); + const closeAlertEvent = new EventWrapper(EVENT_TYPE.CLICK, (() => { + this._toggleAlert(alertElement); + }).bind(this), closeEl); this._eventManager.registerNewListener(closeAlertEvent); diff --git a/frontend/src/utils/mass-input/mass-input.js b/frontend/src/utils/mass-input/mass-input.js index 184706b4d..4969e8c14 100644 --- a/frontend/src/utils/mass-input/mass-input.js +++ b/frontend/src/utils/mass-input/mass-input.js @@ -71,7 +71,7 @@ export class MassInput { }); const submitEv = new EventWrapper(EVENT_TYPE.SUBMIT, this._massInputFormSubmitHandler.bind(this), this._massInputForm); - const keyPressEv = new EventWrapper(EVENT_TYPE.KEYDOWN, this._keypressHandler.bind(this), this.massInputForm); + const keyPressEv = new EventWrapper(EVENT_TYPE.KEYDOWN, this._keypressHandler.bind(this), this._massInputForm); this._eventManager.registerListeners([submitEv, keyPressEv]); Array.from(this._element.querySelectorAll(MASS_INPUT_ADD_CELL_SELECTOR)).forEach(this._setupChangedHandlers.bind(this)); diff --git a/frontend/src/utils/pageactions/pageactions.js b/frontend/src/utils/pageactions/pageactions.js index 870065a4e..88084636b 100644 --- a/frontend/src/utils/pageactions/pageactions.js +++ b/frontend/src/utils/pageactions/pageactions.js @@ -117,7 +117,7 @@ export class PageActionSecondaryUtil { destroy() { this._eventManager.cleanUp(); - if(this._closer && this._closer.classList.contains()) + if(this._closer && this._closer.classList.contains('pagenav-item__close-label--hidden')) this._closer.classList.remove('pagenav-item__close-label--hidden'); this._element.classList.remove(PAGEACTION_SECONDARY_INITIALIZED_CLASS); } diff --git a/frontend/src/utils/show-hide/show-hide.js b/frontend/src/utils/show-hide/show-hide.js index e7f7c3315..411cf1a7d 100644 --- a/frontend/src/utils/show-hide/show-hide.js +++ b/frontend/src/utils/show-hide/show-hide.js @@ -70,9 +70,9 @@ export class ShowHide { } destroy() { + this._storageManager.clear({ location: LOCATION.LOCAL }); this._eventManager.cleanUp(); - this._storageManager.clear(); - if(this._element.parentElement.contains(SHOW_HIDE_COLLAPSED_CLASS)) + if (this._element.parentElement.classList.contains(SHOW_HIDE_COLLAPSED_CLASS)) this._element.parentElement.classList.remove(SHOW_HIDE_COLLAPSED_CLASS); this._element.classList.remove(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS); } diff --git a/frontend/src/utils/tooltips/tooltips.js b/frontend/src/utils/tooltips/tooltips.js index c520af204..2a4d77e9b 100644 --- a/frontend/src/utils/tooltips/tooltips.js +++ b/frontend/src/utils/tooltips/tooltips.js @@ -59,7 +59,7 @@ export class Tooltip { const mouseOutEv = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (this._leave.bind(this)).bind(this), this._element); const contentMouseOut = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (this._leave.bind(this)).bind(this), this._content); const clickEv = new EventWrapper(EVENT_TYPE.CLICK, this._click.bind(this), this._element); - this.registerListeners([mouseOverEv, mouseOutEv, contentMouseOut, clickEv]); + this._eventManager.registerListeners([mouseOverEv, mouseOutEv, contentMouseOut, clickEv]); } open(persistent) { From e5290f0e57c48d9ce9235ef9ac884864aa45d163 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 3 Aug 2021 12:00:45 +0200 Subject: [PATCH 070/143] chore(event-manger): uncommented debuglog --- frontend/src/lib/event-manager/event-manager.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index edc97c91f..a69965a6f 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -62,11 +62,11 @@ export class EventManager { } - //Todo: Uncomment debug log! - //_debugLog() {} - _debugLog(fName, ...args) { - console.log(`[DEBUGLOG] EventManager.${fName}`, { args: args, instance: this }); - } + + _debugLog() {} + //_debugLog(fName, ...args) { + // console.log(`[DEBUGLOG] EventManager.${fName}`, { args: args, instance: this }); + //} } export class EventWrapper { From 8c818a46aba9475d55572f699d1541ac77bff2ac Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 3 Aug 2021 12:14:30 +0200 Subject: [PATCH 071/143] chore(alert): added a start method to alert util --- frontend/src/utils/alerts/alerts.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/utils/alerts/alerts.js b/frontend/src/utils/alerts/alerts.js index 8f7877e68..fd9681f89 100644 --- a/frontend/src/utils/alerts/alerts.js +++ b/frontend/src/utils/alerts/alerts.js @@ -52,25 +52,25 @@ export class Alerts { this._togglerElement = this._element.querySelector('.' + ALERTS_TOGGLER_CLASS); this._alertElements = this._gatherAlertElements(); + // mark initialized + this._element.classList.add(ALERTS_INITIALIZED_CLASS); + } + + start() { if (this._togglerElement) { - //should there be a start method, to initialize the listeners in initToggler and initAlerts or is this wanted? this._initToggler(); } - this._initAlerts(); // register http client interceptor to filter out Alerts Header this._setupHttpInterceptor(); - - // mark initialized - this._element.classList.add(ALERTS_INITIALIZED_CLASS); } destroy() { this._eventManager.removeAllEventListenersFromUtil(); if(this._alertElements) { - this._alertElements.forEach(element => element.remove() ); + this._alertElements.forEach(element => element.remove()); } if(this._element.classList.contains(ALERTS_INITIALIZED_CLASS)) From 0823df33b5f157af290aca9a65f1df429048e53b Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 3 Aug 2021 17:34:28 +0200 Subject: [PATCH 072/143] feat(http-client): added possibility to remove specific interceptors --- frontend/src/services/http-client/http-client.js | 12 ++++++++++++ .../src/services/http-client/http-client.spec.js | 10 +++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/frontend/src/services/http-client/http-client.js b/frontend/src/services/http-client/http-client.js index 274f86cdb..28d7cba21 100644 --- a/frontend/src/services/http-client/http-client.js +++ b/frontend/src/services/http-client/http-client.js @@ -15,6 +15,18 @@ export class HttpClient { } } + removeResponseInterceptor(interceptor) { + //performs a reference check. if the interceptor is bound, when adding it, the reference of the bound function needs to be the same when removing it later. + + if (typeof interceptor !== 'function') { + throw new Error(`Cannot remove Interceptor ${interceptor}, because it is not of type function`); + } + if(this._responseInterceptors.filter(el => el == interceptor).length !== 1) { + throw new Error(`Could not find Response Interceptor ${interceptor}.`); + } + this._responseInterceptors = this._responseInterceptors.filter(el => el != interceptor); + } + _baseUrl; setBaseUrl(baseUrl) { diff --git a/frontend/src/services/http-client/http-client.spec.js b/frontend/src/services/http-client/http-client.spec.js index a0f76584d..594f7096c 100644 --- a/frontend/src/services/http-client/http-client.spec.js +++ b/frontend/src/services/http-client/http-client.spec.js @@ -75,7 +75,7 @@ describe('HttpClient', () => { expect(httpClient._responseInterceptors.length).toBe(2); }); - describe('get called', () => { + describe('get called and removed', () => { let intercepted1; let intercepted2; const interceptors = { @@ -111,6 +111,14 @@ describe('HttpClient', () => { done(); }); }); + + it('can be removed', () => { + expect(httpClient._responseInterceptors.length).toBe(2); + httpClient.removeResponseInterceptor(interceptors.interceptor1); + expect(httpClient._responseInterceptors.length).toBe(1); + expect(() => {httpClient.removeResponseInterceptor(interceptors.interceptor1);}).toThrow(); + expect(httpClient._responseInterceptors.length).toBe(1); + }); }); }); }); From fc7d5dc94e7a2d9181c3c938c37ddaa74727553a Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 3 Aug 2021 17:35:10 +0200 Subject: [PATCH 073/143] chore(alert): removing the interceptor when destroying the util --- frontend/src/utils/alerts/alerts.js | 5 ++++- frontend/src/utils/alerts/alerts.spec.js | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/alerts/alerts.js b/frontend/src/utils/alerts/alerts.js index fd9681f89..bf29445eb 100644 --- a/frontend/src/utils/alerts/alerts.js +++ b/frontend/src/utils/alerts/alerts.js @@ -34,6 +34,7 @@ export class Alerts { _app; _eventManager; + _boundResponseInterceptor; constructor(element, app) { if (!element) { @@ -44,6 +45,7 @@ export class Alerts { this._app = app; this._eventManager = new EventManager(); + this._boundResponseInterceptor = this._responseInterceptor.bind(this); if (this._element.classList.contains(ALERTS_INITIALIZED_CLASS)) { return false; @@ -68,6 +70,7 @@ export class Alerts { destroy() { this._eventManager.removeAllEventListenersFromUtil(); + this._app.httpClient.removeResponseInterceptor(this._boundResponseInterceptor); if(this._alertElements) { this._alertElements.forEach(element => element.remove()); @@ -135,7 +138,7 @@ export class Alerts { } _setupHttpInterceptor() { - this._app.httpClient.addResponseInterceptor(this._responseInterceptor.bind(this)); + this._app.httpClient.addResponseInterceptor(this._boundResponseInterceptor); } _elevateAlerts() { diff --git a/frontend/src/utils/alerts/alerts.spec.js b/frontend/src/utils/alerts/alerts.spec.js index 889229f7c..db14c7361 100644 --- a/frontend/src/utils/alerts/alerts.spec.js +++ b/frontend/src/utils/alerts/alerts.spec.js @@ -3,6 +3,7 @@ import { Alerts, ALERTS_INITIALIZED_CLASS } from './alerts'; const MOCK_APP = { httpClient: { addResponseInterceptor: () => {}, + removeResponseInterceptor: () => {}, }, }; From 14a16c7283483ff22ce22b070a430a61d24a7f35 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 6 Aug 2021 13:50:32 +0200 Subject: [PATCH 074/143] fix(async-form): destroy all after response is processed --- frontend/src/utils/async-form/async-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/async-form/async-form.js b/frontend/src/utils/async-form/async-form.js index 17f2c0810..1365150c6 100644 --- a/frontend/src/utils/async-form/async-form.js +++ b/frontend/src/utils/async-form/async-form.js @@ -60,6 +60,7 @@ export class AsyncForm { setTimeout(() => { parentElement.insertBefore(responseElement, this._element); this._element.remove(); + this._app.utilRegistry.destroyAll(this._element); }, delay); } @@ -102,6 +103,5 @@ export class AsyncForm { this._processResponse({ content: failureMessage }); this._element.classList.remove(ASYNC_FORM_LOADING_CLASS); }); - this._app.utilRegistry.destroyAll(this._element); } } From 9843cdf3c4b7634a10d121b0eb974df202ef7cab Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 6 Aug 2021 15:11:28 +0200 Subject: [PATCH 075/143] chore(async-table): added destroy before new html is set --- frontend/src/utils/async-table/async-table.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index 68785f276..c0536136e 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -447,6 +447,8 @@ export class AsyncTable { this._active = false; this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS); this._element.dataset['currentTableUrl'] = url.href; + + this._app.utilRegistry.destroyAll(this._element); // update table with new this._element.innerHTML = response.element.innerHTML; From ad3bf94c20972db9a437dc9dcda201e86263a6e7 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 6 Aug 2021 15:42:09 +0200 Subject: [PATCH 076/143] chore(async-table): mutation observer is handeled in event manager --- frontend/src/utils/async-table/async-table.js | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index c0536136e..28c4abb00 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -240,17 +240,7 @@ export class AsyncTable { const debouncedUpdateFromTableFilter = throttle((() => this._updateFromTableFilter(tableFilterForm)).bind(this), FILTER_DEBOUNCE, { leading: true, trailing: false }); [...this._tableFilterInputs.search, ...this._tableFilterInputs.input].forEach((input) => { - const submitLockObserver = new MutationObserver((mutations, observer) => { - for (const mutation of mutations) { - // if the submit lock has been released, trigger an update and disconnect this observer - if (mutation.target === input && mutation.attributeName === ATTR_SUBMIT_LOCKED && mutation.oldValue === 'true' && mutation.target.getAttribute(mutation.attributeName) === 'false') { - debouncedUpdateFromTableFilter(); - observer.disconnect(); - break; - } - } - }); - this._cancelPendingUpdates.push(() => { submitLockObserver.disconnect(); }); + this._cancelPendingUpdates.push(() => { this._eventManager.removeAllObserversFromUtil();}); const debouncedInput = debounce(() => { const submitLockedAttr = input.getAttribute(ATTR_SUBMIT_LOCKED); @@ -259,7 +249,16 @@ export class AsyncTable { debouncedUpdateFromTableFilter(); } else if (submitLockedAttr === 'true') { // observe the submit lock of the input element - submitLockObserver.observe(input, { + this._eventManager.registerNewMutationObserver(((mutations, observer) => { + for (const mutation of mutations) { + // if the submit lock has been released, trigger an update and disconnect this observer + if (mutation.target === input && mutation.attributeName === ATTR_SUBMIT_LOCKED && mutation.oldValue === 'true' && mutation.target.getAttribute(mutation.attributeName) === 'false') { + debouncedUpdateFromTableFilter(); + observer.disconnect(); + break; + } + } + }).bind(this), input, { attributes: true, attributeFilter: [ATTR_SUBMIT_LOCKED], attributeOldValue: true, From 7a0715906c8336ed64bbfabdf614746ade9f661f Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 6 Aug 2021 15:50:46 +0200 Subject: [PATCH 077/143] fix(navigate-away-promp): removed unnecessary destroyAll --- frontend/src/utils/form/navigate-away-prompt.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/utils/form/navigate-away-prompt.js b/frontend/src/utils/form/navigate-away-prompt.js index fd671bc81..bb05238aa 100644 --- a/frontend/src/utils/form/navigate-away-prompt.js +++ b/frontend/src/utils/form/navigate-away-prompt.js @@ -112,7 +112,6 @@ export class NavigateAwayPrompt { if(this._unloadDueToSubmit) { - this._app.utilRegistry.destroyAll(this._element); return; } From 0688eef70ccd2fc1f72cf3d3124114948aaf2205 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 6 Aug 2021 15:59:29 +0200 Subject: [PATCH 078/143] chore: cleanUp instead of removeListeners --- frontend/src/lib/event-manager/event-manager.js | 8 ++++---- frontend/src/utils/alerts/alerts.js | 2 +- frontend/src/utils/asidenav/asidenav.js | 2 +- frontend/src/utils/async-form/async-form.js | 2 +- frontend/src/utils/async-table/async-table.js | 2 +- frontend/src/utils/check-all/check-all.js | 2 +- frontend/src/utils/course-teaser/course-teaser.js | 2 +- frontend/src/utils/exam-correct/exam-correct.js | 2 +- frontend/src/utils/form/auto-submit-input.js | 2 +- frontend/src/utils/form/communication-recipients.js | 3 +-- frontend/src/utils/form/datepicker.js | 2 +- frontend/src/utils/form/enter-is-tab.js | 2 +- frontend/src/utils/form/form-error-remover.js | 2 +- frontend/src/utils/form/form-error-reporter.js | 2 +- 14 files changed, 17 insertions(+), 18 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index a69965a6f..851e74eeb 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -63,10 +63,10 @@ export class EventManager { - _debugLog() {} - //_debugLog(fName, ...args) { - // console.log(`[DEBUGLOG] EventManager.${fName}`, { args: args, instance: this }); - //} + //_debugLog() {} + _debugLog(fName, ...args) { + console.log(`[DEBUGLOG] EventManager.${fName}`, { args: args, instance: this }); + } } export class EventWrapper { diff --git a/frontend/src/utils/alerts/alerts.js b/frontend/src/utils/alerts/alerts.js index bf29445eb..a88fd1f19 100644 --- a/frontend/src/utils/alerts/alerts.js +++ b/frontend/src/utils/alerts/alerts.js @@ -69,7 +69,7 @@ export class Alerts { } destroy() { - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); this._app.httpClient.removeResponseInterceptor(this._boundResponseInterceptor); if(this._alertElements) { diff --git a/frontend/src/utils/asidenav/asidenav.js b/frontend/src/utils/asidenav/asidenav.js index 560183e8c..324fc3fd8 100644 --- a/frontend/src/utils/asidenav/asidenav.js +++ b/frontend/src/utils/asidenav/asidenav.js @@ -40,7 +40,7 @@ export class Asidenav { destroy() { - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); if(this._element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) this._element.classList.remove(ASIDENAV_INITIALIZED_CLASS); diff --git a/frontend/src/utils/async-form/async-form.js b/frontend/src/utils/async-form/async-form.js index 1365150c6..482cdb634 100644 --- a/frontend/src/utils/async-form/async-form.js +++ b/frontend/src/utils/async-form/async-form.js @@ -44,7 +44,7 @@ export class AsyncForm { } destroy() { - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); if(this._element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS)) this._element.classList.remove(ASYNC_FORM_INITIALIZED_CLASS); diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index 28c4abb00..6ddf4536a 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -150,7 +150,7 @@ export class AsyncTable { destroy() { this._windowStorage.clear(this._windowStorage._options); - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); this._active = false; if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS); diff --git a/frontend/src/utils/check-all/check-all.js b/frontend/src/utils/check-all/check-all.js index 7d051cddd..79558a75f 100644 --- a/frontend/src/utils/check-all/check-all.js +++ b/frontend/src/utils/check-all/check-all.js @@ -48,7 +48,7 @@ export class CheckAll { } destroy() { - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); this._checkAllColumns.forEach((column) => { if (column._checkAllCheckBox !== undefined) column._checkAllCheckBox.remove(); diff --git a/frontend/src/utils/course-teaser/course-teaser.js b/frontend/src/utils/course-teaser/course-teaser.js index 95a49faee..31d8cf225 100644 --- a/frontend/src/utils/course-teaser/course-teaser.js +++ b/frontend/src/utils/course-teaser/course-teaser.js @@ -30,7 +30,7 @@ export class CourseTeaser { } destroy() { - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); if(this._element.classList.contains(COURSE_TEASER_EXPANDED_CLASS)) { this._element.classList.remove(COURSE_TEASER_EXPANDED_CLASS); } diff --git a/frontend/src/utils/exam-correct/exam-correct.js b/frontend/src/utils/exam-correct/exam-correct.js index c88ce2d6b..f078f9825 100644 --- a/frontend/src/utils/exam-correct/exam-correct.js +++ b/frontend/src/utils/exam-correct/exam-correct.js @@ -172,7 +172,7 @@ export class ExamCorrect { } destroy() { - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); } _updatePartDeleteDisabled(deleteBox) { diff --git a/frontend/src/utils/form/auto-submit-input.js b/frontend/src/utils/form/auto-submit-input.js index 8ec7e869e..291a1d3aa 100644 --- a/frontend/src/utils/form/auto-submit-input.js +++ b/frontend/src/utils/form/auto-submit-input.js @@ -45,7 +45,7 @@ export class AutoSubmitInput { } destroy() { - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); if(this._element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS)) this._element.classList.remove(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS); } diff --git a/frontend/src/utils/form/communication-recipients.js b/frontend/src/utils/form/communication-recipients.js index dc04f17cc..b6d754e09 100644 --- a/frontend/src/utils/form/communication-recipients.js +++ b/frontend/src/utils/form/communication-recipients.js @@ -44,8 +44,7 @@ export class CommunicationRecipients { } destroy() { - this._eventManager.removeAllEventListenersFromUtil(); - this._eventManager.removeAllObserversFromUtil(); + this._eventManager.cleanUp(); this._removeCheckedCounter(); } diff --git a/frontend/src/utils/form/datepicker.js b/frontend/src/utils/form/datepicker.js index 7a8abe3ad..c6273c07b 100644 --- a/frontend/src/utils/form/datepicker.js +++ b/frontend/src/utils/form/datepicker.js @@ -243,7 +243,7 @@ export class Datepicker { destroy() { this.datepickerInstance.remove(); - this._eventManager.removeAllListenersFromUtil(); + this._eventManager.cleanUp(); this._element.classList.remove(DATEPICKER_INITIALIZED_CLASS); } diff --git a/frontend/src/utils/form/enter-is-tab.js b/frontend/src/utils/form/enter-is-tab.js index 0a3073b9e..b3097d50d 100644 --- a/frontend/src/utils/form/enter-is-tab.js +++ b/frontend/src/utils/form/enter-is-tab.js @@ -56,7 +56,7 @@ export class EnterIsTab { } destroy() { - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); if(this._element.classList.contains(ENTER_IS_TAB_INITIALIZED_CLASS)) this._element.classList.remove(ENTER_IS_TAB_INITIALIZED_CLASS); } diff --git a/frontend/src/utils/form/form-error-remover.js b/frontend/src/utils/form/form-error-remover.js index 5fc1f90c0..f05e96fae 100644 --- a/frontend/src/utils/form/form-error-remover.js +++ b/frontend/src/utils/form/form-error-remover.js @@ -49,7 +49,7 @@ export class FormErrorRemover { } destroy() { - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); this._element.classList.remove(FORM_ERROR_REMOVER_INITIALIZED_CLASS); } diff --git a/frontend/src/utils/form/form-error-reporter.js b/frontend/src/utils/form/form-error-reporter.js index 8ebd14d5f..ed3012b26 100644 --- a/frontend/src/utils/form/form-error-reporter.js +++ b/frontend/src/utils/form/form-error-reporter.js @@ -38,7 +38,7 @@ export class FormErrorReporter { } destroy() { - this._eventManager.removeAllEventListenersFromUtil(); + this._eventManager.cleanUp(); this._removeError(); From 204ce39f7c2f9c405971aa59da068a5389dda417 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 6 Aug 2021 18:39:40 +0200 Subject: [PATCH 079/143] fix(password): added cleanUP --- frontend/src/utils/async-table/async-table.js | 8 ++++---- frontend/src/utils/inputs/password.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index 6ddf4536a..1734de1b1 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -462,10 +462,10 @@ export class AsyncTable { ).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS)); } - //_debugLog() {} - _debugLog(fName, ...args) { - console.log(`[DEBUGLOG] AsyncTable.${fName}`, { args: args, instance: this }); - } + _debugLog() {} + //_debugLog(fName, ...args) { + // console.log(`[DEBUGLOG] AsyncTable.${fName}`, { args: args, instance: this }); + // } } diff --git a/frontend/src/utils/inputs/password.js b/frontend/src/utils/inputs/password.js index 3793598ee..0659ab57e 100644 --- a/frontend/src/utils/inputs/password.js +++ b/frontend/src/utils/inputs/password.js @@ -67,6 +67,7 @@ export class Password { } destroy() { + this._eventManager.cleanUp(); this._iconEl.remove(); this._toggleContainerEl.remove(); this._wrapperEl.remove(); From 945368972da36f4ff0da99f38c71ea9a1c7157d7 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Thu, 12 Aug 2021 21:47:44 +0200 Subject: [PATCH 080/143] fix(show-hide): storage manager is not cleared --- frontend/src/utils/show-hide/show-hide.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/utils/show-hide/show-hide.js b/frontend/src/utils/show-hide/show-hide.js index 411cf1a7d..5739baac9 100644 --- a/frontend/src/utils/show-hide/show-hide.js +++ b/frontend/src/utils/show-hide/show-hide.js @@ -70,7 +70,6 @@ export class ShowHide { } destroy() { - this._storageManager.clear({ location: LOCATION.LOCAL }); this._eventManager.cleanUp(); if (this._element.parentElement.classList.contains(SHOW_HIDE_COLLAPSED_CLASS)) this._element.parentElement.classList.remove(SHOW_HIDE_COLLAPSED_CLASS); From b3b0d6585068ecbc665e819b31c94f4d96a0fec5 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Thu, 12 Aug 2021 21:53:30 +0200 Subject: [PATCH 081/143] fix(hide-columns): removed clear storage from destroy method --- frontend/src/utils/hide-columns/hide-columns.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index bb2487c38..79390402a 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -91,7 +91,6 @@ export class HideColumns { destroy() { this._eventManager.cleanUp(); - this._storageManager.clear(); this._tableUtilContainer.remove(); this._element.classList.remove(HIDE_COLUMNS_INITIALIZED); } From acdeea0b3d8d934a6df131cd44e129f8c87a9c5b Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Thu, 12 Aug 2021 22:03:58 +0200 Subject: [PATCH 082/143] chore(event-manager): uncommented log --- frontend/src/lib/event-manager/event-manager.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js index 851e74eeb..a69965a6f 100644 --- a/frontend/src/lib/event-manager/event-manager.js +++ b/frontend/src/lib/event-manager/event-manager.js @@ -63,10 +63,10 @@ export class EventManager { - //_debugLog() {} - _debugLog(fName, ...args) { - console.log(`[DEBUGLOG] EventManager.${fName}`, { args: args, instance: this }); - } + _debugLog() {} + //_debugLog(fName, ...args) { + // console.log(`[DEBUGLOG] EventManager.${fName}`, { args: args, instance: this }); + //} } export class EventWrapper { From f1c50e137f4f11140c1171dd9d86bc5511b9e771 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Thu, 12 Aug 2021 22:30:42 +0200 Subject: [PATCH 083/143] fix(storage-manager): clear is working without options as well --- frontend/src/lib/storage-manager/storage-manager.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js index f2ee68589..2068cd02b 100644 --- a/frontend/src/lib/storage-manager/storage-manager.js +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -186,14 +186,14 @@ export class StorageManager { } } - clear(options) { + clear(options=this._options) { this._debugLog('clear', options); if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) { throw new Error('StorageManager.clear called with unsupported location option'); } - const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING; + const locations = ((options !== undefined) && options.location !== undefined)? [options.location] : this._location_shadowing; for (const location of locations) { switch (location) { @@ -204,7 +204,10 @@ export class StorageManager { case LOCATION.WINDOW: return this._clearWindow(); case LOCATION.HISTORY: - return this._clearHistory(options && options.history); + if(options.history) + return this._clearHistory(options && options.history); + else + return; default: console.error('StorageManager.clear cannot clear with unsupported location'); } From 49ac17f8e9efac85c8bf4e581153a9da469a0fb7 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Thu, 12 Aug 2021 22:34:10 +0200 Subject: [PATCH 084/143] chore(model): moved destroyAll into close method --- frontend/src/utils/modal/modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/modal/modal.js b/frontend/src/utils/modal/modal.js index f45770d10..8f013572f 100644 --- a/frontend/src/utils/modal/modal.js +++ b/frontend/src/utils/modal/modal.js @@ -137,7 +137,6 @@ export class Modal { _onCloseClicked = (event) => { event.preventDefault(); this._close(); - this._app.utilRegistry.destroyAll(this._element); } _onKeyUp = (event) => { @@ -165,6 +164,7 @@ export class Modal { this._modalsWrapper.classList.remove(MODALS_WRAPPER_OPEN_CLASS); document.removeEventListener('keyup', this._onKeyUp); + this._app.utilRegistry.destroyAll(this._element); }; _fillModal(url) { From a5e666d155e6f9c998f3fb4fadf97d01e68f2b45 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 24 Aug 2021 10:40:51 +0200 Subject: [PATCH 085/143] chore(navigate-away-prompt): merging two if conditions --- frontend/src/utils/form/navigate-away-prompt.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/src/utils/form/navigate-away-prompt.js b/frontend/src/utils/form/navigate-away-prompt.js index bb05238aa..da900ba72 100644 --- a/frontend/src/utils/form/navigate-away-prompt.js +++ b/frontend/src/utils/form/navigate-away-prompt.js @@ -107,13 +107,8 @@ export class NavigateAwayPrompt { // allow the event to happen if the form was not touched by the // user (i.e. if the current FormData is equal to the initial FormData) // or the unload event was initiated by a form submit - if (!formDataHasChanged) + if (!formDataHasChanged || this.unloadDueToSubmit) return; - - - if(this._unloadDueToSubmit) { - return; - } // cancel the unload event. This is the standard to force the prompt to appear. event.preventDefault(); From cbc03f57c56df9c96a84da15a68d26904b75a65f Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 24 Aug 2021 10:44:24 +0200 Subject: [PATCH 086/143] fix(util-registry): handle negative indices correctly --- frontend/src/services/util-registry/util-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index 67cc09702..2e6115dc0 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -106,7 +106,7 @@ export class UtilRegistry { } let utilIndex = this._activeUtilInstancesWrapped.indexOf(util); util.destroy(); - this._activeUtilInstancesWrapped.splice(utilIndex, 1); + if (utilIndex >= 0) this._activeUtilInstancesWrapped.splice(utilIndex, 1); }); } From 5078b56c16513f3b289bc4824ce0c7914b1a220d Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 24 Aug 2021 10:46:58 +0200 Subject: [PATCH 087/143] fix(http-client): strict equality check --- frontend/src/services/http-client/http-client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/services/http-client/http-client.js b/frontend/src/services/http-client/http-client.js index 28d7cba21..34fba00dc 100644 --- a/frontend/src/services/http-client/http-client.js +++ b/frontend/src/services/http-client/http-client.js @@ -24,7 +24,7 @@ export class HttpClient { if(this._responseInterceptors.filter(el => el == interceptor).length !== 1) { throw new Error(`Could not find Response Interceptor ${interceptor}.`); } - this._responseInterceptors = this._responseInterceptors.filter(el => el != interceptor); + this._responseInterceptors = this._responseInterceptors.filter(el => el !== interceptor); } _baseUrl; From 5b4ac7587438e7adb21e0ff3d9d629b6ff00263a Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 24 Aug 2021 12:15:13 +0200 Subject: [PATCH 088/143] fix(util-registry): filtering activeUtilInstances when a util is destroyed --- frontend/src/services/util-registry/util-registry.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index 2e6115dc0..2b0005a35 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -104,9 +104,10 @@ export class UtilRegistry { if(DEBUG_MODE > 2) { console.log('Destroying Util: ', {util}); } - let utilIndex = this._activeUtilInstancesWrapped.indexOf(util); util.destroy(); - if (utilIndex >= 0) this._activeUtilInstancesWrapped.splice(utilIndex, 1); + this._activeUtilInstancesWrapped = this._activeUtilInstancesWrapped.filter(utilWrapped => { + return utilWrapped.element === util._element; + }); }); } From f19e9bab9285e090888f0898b47d1d54d18360e0 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 24 Aug 2021 14:38:26 +0200 Subject: [PATCH 089/143] chore(tooltips): all tooltips classes are removed in a loop --- frontend/src/utils/tooltips/tooltips.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/utils/tooltips/tooltips.js b/frontend/src/utils/tooltips/tooltips.js index 2a4d77e9b..1bd831c59 100644 --- a/frontend/src/utils/tooltips/tooltips.js +++ b/frontend/src/utils/tooltips/tooltips.js @@ -190,13 +190,12 @@ export class Tooltip { destroy() { this._eventManager.cleanUp(); - if(this._element.classList.contains(TOOLTIP_OPEN_CLASS)) - this._element.classList.remove(TOOLTIP_OPEN_CLASS); - if(this._element.classList.contains('tooltip--right')) - this._element.classList.remove('tooltip--right'); - if(this._element.classList.contains('tooltip--bottom')) - this._element.classList.remove('tooltip--bottom'); this._movementObserver.unobserve(); - this._element.classList.remove(TOOLTIP_INITIALIZED_CLASS); + for (let i = 0; i < this._element.classList.length; i++) { + if (/tooltip--*/.test(this._element.classList[i])) + this._element.classList.remove(this._element.classList.item(i)); + else + i++; + } } }; From 8d0241e727efb5051e5848f2a0c835d7a8d3ae6b Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 24 Aug 2021 14:39:52 +0200 Subject: [PATCH 090/143] fix(tooltips): removed else case --- frontend/src/utils/tooltips/tooltips.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/utils/tooltips/tooltips.js b/frontend/src/utils/tooltips/tooltips.js index 1bd831c59..ba32bab9e 100644 --- a/frontend/src/utils/tooltips/tooltips.js +++ b/frontend/src/utils/tooltips/tooltips.js @@ -194,8 +194,6 @@ export class Tooltip { for (let i = 0; i < this._element.classList.length; i++) { if (/tooltip--*/.test(this._element.classList[i])) this._element.classList.remove(this._element.classList.item(i)); - else - i++; } } }; From dc0d141cd6bf1394f20e91e2f412b70e32f1ca74 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 24 Aug 2021 14:43:34 +0200 Subject: [PATCH 091/143] chore(http-client): check if length is zero --- frontend/src/services/http-client/http-client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/services/http-client/http-client.js b/frontend/src/services/http-client/http-client.js index 34fba00dc..81f72485e 100644 --- a/frontend/src/services/http-client/http-client.js +++ b/frontend/src/services/http-client/http-client.js @@ -21,7 +21,7 @@ export class HttpClient { if (typeof interceptor !== 'function') { throw new Error(`Cannot remove Interceptor ${interceptor}, because it is not of type function`); } - if(this._responseInterceptors.filter(el => el == interceptor).length !== 1) { + if(this._responseInterceptors.filter(el => el == interceptor).length === 0) { throw new Error(`Could not find Response Interceptor ${interceptor}.`); } this._responseInterceptors = this._responseInterceptors.filter(el => el !== interceptor); From 05de310ddd442f1e52ad88ea204179966305cae6 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 24 Aug 2021 14:49:45 +0200 Subject: [PATCH 092/143] Apply 2 suggestion(s) to 1 file(s) --- frontend/src/lib/storage-manager/storage-manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js index 2068cd02b..d9ef366ec 100644 --- a/frontend/src/lib/storage-manager/storage-manager.js +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -204,8 +204,8 @@ export class StorageManager { case LOCATION.WINDOW: return this._clearWindow(); case LOCATION.HISTORY: - if(options.history) - return this._clearHistory(options && options.history); + if(options && options.history) + return this._clearHistory(options.history); else return; default: From c8d36ea52dbef0a2e1aaec6aedaf7edd524975dc Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 30 Aug 2021 11:32:24 +0200 Subject: [PATCH 093/143] fix(tooltips): correct regex match --- frontend/src/utils/tooltips/tooltips.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/utils/tooltips/tooltips.js b/frontend/src/utils/tooltips/tooltips.js index ba32bab9e..99b144f7e 100644 --- a/frontend/src/utils/tooltips/tooltips.js +++ b/frontend/src/utils/tooltips/tooltips.js @@ -191,9 +191,7 @@ export class Tooltip { destroy() { this._eventManager.cleanUp(); this._movementObserver.unobserve(); - for (let i = 0; i < this._element.classList.length; i++) { - if (/tooltip--*/.test(this._element.classList[i])) - this._element.classList.remove(this._element.classList.item(i)); - } + const toolTipsRegex = RegExp(/\btooltip--.+\b/, 'g'); + this._element.className = this._element.className.replace(toolTipsRegex, ''); } }; From ebcb23429ff09c56ba4a00a6cb8f082ee83e1fe8 Mon Sep 17 00:00:00 2001 From: ros Date: Sun, 22 Aug 2021 15:49:37 +0200 Subject: [PATCH 094/143] feat(tutoriumsdaten): termin --- src/Handler/Tutorial/Users.hs | 2 +- templates/tutorial-participants.hamlet | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Handler/Tutorial/Users.hs b/src/Handler/Tutorial/Users.hs index f8215a0d9..cc34b1d6a 100644 --- a/src/Handler/Tutorial/Users.hs +++ b/src/Handler/Tutorial/Users.hs @@ -55,7 +55,7 @@ postTUsersR tid ssh csh tutn = do cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh table <- makeCourseUserTable cid (Map.fromList $ map (id &&& pure) universeF) isInTut colChoices psValidator (Just csvColChoices) return (tut, table) - + formResult participantRes $ \case (TutorialUserSendMail, selectedUsers) -> do cids <- traverse encrypt $ Set.toList selectedUsers :: Handler [CryptoUUIDUser] diff --git a/templates/tutorial-participants.hamlet b/templates/tutorial-participants.hamlet index 1c6999f09..697d9f6a7 100644 --- a/templates/tutorial-participants.hamlet +++ b/templates/tutorial-participants.hamlet @@ -1,2 +1,8 @@ $newline never +
+
+
_{MsgTableTutorialTime} +
+ ^{occurrencesWidget tutorialTime} +
_{MsgTableTutorialTutors} ^{participantTable} From e972788f540a9ce6c3fdf841313057b62a579d72 Mon Sep 17 00:00:00 2001 From: ros Date: Sun, 29 Aug 2021 14:14:21 +0200 Subject: [PATCH 095/143] feat(tutoriumsdaten): firts draft --- src/Application.hs | 1 + src/Handler/Tutorial/Users.hs | 8 +++++++- templates/tutorial-participants.hamlet | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Application.hs b/src/Application.hs index 7d02e6009..e4e2d4b1c 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -719,3 +719,4 @@ addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do PWHashConf{..} <- getsYesod $ view _appAuthPWHash (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength void $ insert User{..} + diff --git a/src/Handler/Tutorial/Users.hs b/src/Handler/Tutorial/Users.hs index cc34b1d6a..eb15e4e84 100644 --- a/src/Handler/Tutorial/Users.hs +++ b/src/Handler/Tutorial/Users.hs @@ -55,7 +55,7 @@ postTUsersR tid ssh csh tutn = do cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh table <- makeCourseUserTable cid (Map.fromList $ map (id &&& pure) universeF) isInTut colChoices psValidator (Just csvColChoices) return (tut, table) - + formResult participantRes $ \case (TutorialUserSendMail, selectedUsers) -> do cids <- traverse encrypt $ Set.toList selectedUsers :: Handler [CryptoUUIDUser] @@ -67,6 +67,12 @@ postTUsersR tid ssh csh tutn = do ] addMessageI Success $ MsgTutorialUsersDeregistered nrDel redirect $ CTutorialR tid ssh csh tutn TUsersR + + tutors <- runDB $ + E.select $ E.from $ \(tutor `E.InnerJoin` user) -> do + E.on $ tutor E.^. TutorUser E.==. user E.^. UserId + E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid + return user let heading = prependCourseTitle tid ssh csh $ CI.original tutorialName siteLayoutMsg heading $ do diff --git a/templates/tutorial-participants.hamlet b/templates/tutorial-participants.hamlet index 697d9f6a7..a5ca27d35 100644 --- a/templates/tutorial-participants.hamlet +++ b/templates/tutorial-participants.hamlet @@ -5,4 +5,9 @@ $newline never
^{occurrencesWidget tutorialTime}
_{MsgTableTutorialTutors} +
+
    + $forall (Entity _ User{userDisplayName, userDisplayEmail, userSurname}) <- tutors +
  • + ^{nameEmailWidget userDisplayEmail userDisplayName userSurname} ^{participantTable} From d4a73e699a399b02cadcea03e614c789671ee6d1 Mon Sep 17 00:00:00 2001 From: ros Date: Sun, 29 Aug 2021 14:20:39 +0200 Subject: [PATCH 096/143] feat(tutoriumsdaten): application restore --- src/Application.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Application.hs b/src/Application.hs index e4e2d4b1c..7d02e6009 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -719,4 +719,3 @@ addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do PWHashConf{..} <- getsYesod $ view _appAuthPWHash (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength void $ insert User{..} - From 08f9bc06974410bf2ed128a22e7c243336e44727 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Mon, 30 Aug 2021 17:04:12 +0200 Subject: [PATCH 097/143] chore(release): 25.22.0 --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- package.yaml | 2 +- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a504a9a..dcf5299d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.22.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.0...v25.22.0) (2021-08-30) + + +### Features + +* **event-manager:** added method to register a list of listeners ([1a8fb23](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1a8fb230441f73b9b1fe593f4df1005a06d9628e)) +* **event-manager:** mutation observers can be managed via the event manager ([34b4f48](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/34b4f48386c8fff4569b12eb3f7919e7c77d33c0)) +* **http-client:** added possibility to remove specific interceptors ([0823df3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0823df33b5f157af290aca9a65f1df429048e53b)) +* **tutoriumsdaten:** application restore ([d4a73e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4a73e699a399b02cadcea03e614c789671ee6d1)) +* **tutoriumsdaten:** firts draft ([e972788](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e972788f540a9ce6c3fdf841313057b62a579d72)) +* **tutoriumsdaten:** termin ([ebcb234](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ebcb23429ff09c56ba4a00a6cb8f082ee83e1fe8)) +* **util_registry:** impelmented destroyAll(scope) method in the utilRegistry ([f1ef2e5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f1ef2e5ec776ad8a1fb7737eb0ed4f79218afd61)) +* implemented an event manager ([c1c3536](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c1c35369d1ae017971e6d8cbafc06844f02fd00d)) + + +### Bug Fixes + +* **async-form:** destroy all after response is processed ([14a16c7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/14a16c7283483ff22ce22b070a430a61d24a7f35)) +* **communication-recipients:** fixed undefined error with context and a few minor issues ([03ac803](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03ac80342e0f3fcb1db2adf34f86a2c0d8fabf0f)) +* **enter-is-tab.js:** implemented destroy method in enter-is-tab Util ([d1b9952](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d1b995269060c670d85f715671bfc9947b9f3e9a)) +* **hide-columns:** removed clear storage from destroy method ([b3b0d65](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b3b0d6585068ecbc665e819b31c94f4d96a0fec5)) +* **hide-colums:** small fix ([50a3ac1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50a3ac1790f26c1e6f983d3687352d7811173110)) +* **http-client:** strict equality check ([5078b56](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5078b56c16513f3b289bc4824ce0c7914b1a220d)) +* **interactive-fieldset:** small fix ([4c2c683](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4c2c68327e75f5f51271853159c232fdd7bba21e)) +* **navigate-away-promp:** removed unnecessary destroyAll ([7a07159](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7a0715906c8336ed64bbfabdf614746ade9f661f)) +* **password:** added cleanUP ([204ce39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/204ce39f7c2f9c405971aa59da068a5389dda417)) +* **show-hide:** storage manager is not cleared ([9453689](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/945368972da36f4ff0da99f38c71ea9a1c7157d7)) +* **storage-manager:** clear is working without options as well ([f1c50e1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f1c50e137f4f11140c1171dd9d86bc5511b9e771)) +* **tooltips:** correct regex match ([c8d36ea](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c8d36ea52dbef0a2e1aaec6aedaf7edd524975dc)) +* **tooltips:** removed else case ([8d0241e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8d0241e727efb5051e5848f2a0c835d7a8d3ae6b)) +* **util-registry:** filtering activeUtilInstances when a util is destroyed ([5b4ac75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5b4ac7587438e7adb21e0ff3d9d629b6ff00263a)) +* **util-registry:** handle negative indices correctly ([cbc03f5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cbc03f57c56df9c96a84da15a68d26904b75a65f)) +* fixed a few minor issues ([6320cd9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6320cd927a84445f056dc782fb440d276cb26009)) +* prompt not shwowing up after submit/close ([abe8415](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/abe84156d508dca8fce549b24d5902d24afc0dbf)) +* smaller fixes and typos ([1f978e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1f978e65a82a91fb728a7ee2970a4fd9e6beb521)) + ## [25.21.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.20.2...v25.21.0) (2021-08-20) diff --git a/package-lock.json b/package-lock.json index 38b149cfa..517e89556 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.21.0", + "version": "25.22.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 727983d52..2e56de4a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.21.0", + "version": "25.22.0", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index 2793c89b4..5c6556bfb 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.21.0 +version: 25.22.0 dependencies: - base - yesod From f1fe4447fbe7e96e55aaf284c7083338b5135ab6 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 17 Sep 2021 13:21:15 +0200 Subject: [PATCH 098/143] fix(course-admins): display course admins as admins instead of assistants --- .../categories/courses/courses/de-de-formal.msg | 1 + .../uniworx/categories/courses/courses/en-eu.msg | 1 + src/Handler/Course/Show.hs | 12 ++++++------ templates/course.hamlet | 7 +++++++ 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/messages/uniworx/categories/courses/courses/de-de-formal.msg b/messages/uniworx/categories/courses/courses/de-de-formal.msg index 92823ea08..2e1880882 100644 --- a/messages/uniworx/categories/courses/courses/de-de-formal.msg +++ b/messages/uniworx/categories/courses/courses/de-de-formal.msg @@ -187,6 +187,7 @@ LecturerFor: Dozent:in LecturersFor: Dozierende AssistantFor: Assistent:in AssistantsFor: Assistent:innen +CourseAdminFor: Kursadministration TutorsFor n@Int: #{pluralDE n "Tutor:in" "Tutor:innen"} CorrectorsFor n@Int: #{pluralDE n "Korrektor:in" "Korrektor:innen"} CourseParticipantsHeading: Kursteilnehmer:innen diff --git a/messages/uniworx/categories/courses/courses/en-eu.msg b/messages/uniworx/categories/courses/courses/en-eu.msg index da740b3ae..c4eda4efc 100644 --- a/messages/uniworx/categories/courses/courses/en-eu.msg +++ b/messages/uniworx/categories/courses/courses/en-eu.msg @@ -187,6 +187,7 @@ LecturerFor: Lecturer LecturersFor: Lecturers AssistantFor: Assistant AssistantsFor: Assistants +CourseAdminFor: Course administration TutorsFor n: #{pluralEN n "Tutor" "Tutors"} CorrectorsFor n: #{pluralEN n "Corrector" "Correctors"} CourseParticipantsHeading: Course participants diff --git a/src/Handler/Course/Show.hs b/src/Handler/Course/Show.hs index fab484c0b..1f25a0b29 100644 --- a/src/Handler/Course/Show.hs +++ b/src/Handler/Course/Show.hs @@ -30,7 +30,7 @@ getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCShowR tid ssh csh = do mbAid <- maybeAuthId now <- liftIO getCurrentTime - (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,mApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister,(mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial)) <- runDB . maybeT notFound $ do + (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,administrators,correctors,tutors,mAllocation,mApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister,(mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial)) <- runDB . maybeT notFound $ do [(E.Entity cid course, E.Value courseVisible, E.Value schoolName, E.Value participants, fmap entityVal -> registration, E.Value hasAllocationRegistrationOpen)] <- lift . E.select . E.from $ \((school `E.InnerJoin` course) `E.LeftOuterJoin` allocation `E.LeftOuterJoin` participant) -> do @@ -62,10 +62,10 @@ getCShowR tid ssh csh = do E.orderBy [ E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName ] return ( lecturer E.^. LecturerType , user E.^. UserDisplayEmail, user E.^. UserDisplayName, user E.^. UserSurname) - let partStaff :: (LecturerType, UserEmail, Text, Text) -> Either (UserEmail, Text, Text) (UserEmail, Text, Text) - partStaff (CourseLecturer ,name,surn,mail) = Right (name,surn,mail) - partStaff (_courseAssistant,name,surn,mail) = Left (name,surn,mail) - (assistants,lecturers) = partitionWith partStaff $ map $(unValueN 4) staff + let + (administrators', regularStaff) = partition ((==) CourseAdministrator . view _1) $ map (\(E.Value lecType, E.Value lecName, E.Value lecSurn, E.Value lecMail) -> (lecType,(lecName,lecSurn,lecMail))) staff + (lecturers', assistants') = partition ((==) CourseLecturer . view _1) regularStaff + (administrators, lecturers, assistants) = (view _2 <$> administrators', view _2 <$> lecturers', view _2 <$> assistants') correctors <- fmap (map $(unValueN 3)) . lift . E.select $ E.from $ \(sheet `E.InnerJoin` sheetCorrector `E.InnerJoin` user) -> E.distinctOnOrderBy [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName, E.asc $ user E.^. UserEmail ] $ do E.on $ sheetCorrector E.^. SheetCorrectorUser E.==. user E.^. UserId E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId @@ -142,7 +142,7 @@ getCShowR tid ssh csh = do return $ material E.^. MaterialName mayViewAnyMaterial <- lift . anyM materials $ \(E.Value mnm) -> hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR - return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,mApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister, (mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial)) + return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,administrators,correctors,tutors,mAllocation,mApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister, (mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial)) let mDereg' = maybe id min (allocationOverrideDeregister =<< mAllocation) <$> courseDeregisterUntil course mDereg <- traverse (formatTime SelFormatDateTime) mDereg' diff --git a/templates/course.hamlet b/templates/course.hamlet index 2205d1f73..de6452829 100644 --- a/templates/course.hamlet +++ b/templates/course.hamlet @@ -93,6 +93,13 @@ $# #{summary}
      $forall assi <- assistants
    • ^{nameEmailWidget' assi} + $with numadmins <- length administrators + $if numadmins /= 0 +
      _{MsgCourseAdminFor} +
      +
        + $forall admin <- administrators +
      • ^{nameEmailWidget' admin} $with numtutor <- length tutors $if numtutor /= 0 From da1c8b54510ee1436fefe97ba32372a08299b83e Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Fri, 17 Sep 2021 17:59:42 +0200 Subject: [PATCH 099/143] feat(check-all): added shift click functionality --- frontend/src/utils/check-all/check-all.js | 36 ++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/check-all/check-all.js b/frontend/src/utils/check-all/check-all.js index b9796eeb5..e1bef2b09 100644 --- a/frontend/src/utils/check-all/check-all.js +++ b/frontend/src/utils/check-all/check-all.js @@ -18,6 +18,8 @@ export class CheckAll { _tableIndices; + _lastCheckedCell = null; + constructor(element, app) { if (!element) { throw new Error('Check All utility cannot be setup without an element!'); @@ -35,13 +37,44 @@ export class CheckAll { if (DEBUG_MODE > 0) console.log(this._columns); + + //Todo: 1 forEach loop - this._findCheckboxColumns().forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId]))); + let checkboxColumns = this._findCheckboxColumns(); + + checkboxColumns.forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId]))); + + checkboxColumns.forEach(columnId => { + let currentColumn = this._columns[columnId]; + currentColumn.forEach(el => el.addEventListener('click', (ev) => { + + if(ev.shiftKey && this.lastCheckedCell !== null) { + let lastClickedIndex = this._tableIndices.rowIndex(this._lastCheckedCell); + let currentCellIndex = this._tableIndices.rowIndex(el); + if(currentCellIndex > lastClickedIndex) + this._checkMultipleCells(lastClickedIndex, currentCellIndex, columnId); + else + this._checkMultipleCells(currentCellIndex, lastClickedIndex, columnId); + } else { + this._lastCheckedCell = el; + } + })); + + }); // mark initialized this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS); } + _checkMultipleCells(firstRowIndex, lastRowIndex, columnId) { + for(let i=firstRowIndex; i<=lastRowIndex; i++) { + let cell = this._columns[columnId][i]; + if (cell.tagName !== 'TH') { + cell.querySelector(CHECKBOX_SELECTOR).checked = true; + } + } + } + _gatherColumns() { for (const rowIndex of Array(this._tableIndices.maxRow + 1).keys()) { for (const colIndex of Array(this._tableIndices.maxCol + 1).keys()) { @@ -97,6 +130,7 @@ class CheckAllColumn { this._checkAllCheckbox = document.createElement('input'); this._checkAllCheckbox.setAttribute('type', 'checkbox'); this._checkAllCheckbox.setAttribute('id', this._checkboxId); + th.insertBefore(this._checkAllCheckbox, th.firstChild); // set up new checkbox From 7d8fc4301bedee4d0f990a1ef5d657f457598a2f Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 21 Sep 2021 15:01:06 +0200 Subject: [PATCH 100/143] chore(check-range): added readme.md for checkrange util --- frontend/src/utils/inputs/checkrange.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 frontend/src/utils/inputs/checkrange.md diff --git a/frontend/src/utils/inputs/checkrange.md b/frontend/src/utils/inputs/checkrange.md new file mode 100644 index 000000000..894a3bd4f --- /dev/null +++ b/frontend/src/utils/inputs/checkrange.md @@ -0,0 +1,5 @@ +# Checkrange Utility +Is set on the table header of a specific row. Remembers the last checked checkbox. When the users shift-clicks another checkbox in the same row, all checkboxes in between are also checked. + +# Attribute: table:not([uw-no-check-all] +(will be setup on all tables which use the util check-all) \ No newline at end of file From c28dca7b76f60c9c34da845ef1322ae0e839d79f Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 21 Sep 2021 17:15:32 +0200 Subject: [PATCH 101/143] chore(check-all): removed checkrange from checkall --- frontend/src/utils/check-all/check-all.js | 28 ----------------------- 1 file changed, 28 deletions(-) diff --git a/frontend/src/utils/check-all/check-all.js b/frontend/src/utils/check-all/check-all.js index e1bef2b09..94115ffee 100644 --- a/frontend/src/utils/check-all/check-all.js +++ b/frontend/src/utils/check-all/check-all.js @@ -37,44 +37,16 @@ export class CheckAll { if (DEBUG_MODE > 0) console.log(this._columns); - - //Todo: 1 forEach loop let checkboxColumns = this._findCheckboxColumns(); checkboxColumns.forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId]))); - checkboxColumns.forEach(columnId => { - let currentColumn = this._columns[columnId]; - currentColumn.forEach(el => el.addEventListener('click', (ev) => { - - if(ev.shiftKey && this.lastCheckedCell !== null) { - let lastClickedIndex = this._tableIndices.rowIndex(this._lastCheckedCell); - let currentCellIndex = this._tableIndices.rowIndex(el); - if(currentCellIndex > lastClickedIndex) - this._checkMultipleCells(lastClickedIndex, currentCellIndex, columnId); - else - this._checkMultipleCells(currentCellIndex, lastClickedIndex, columnId); - } else { - this._lastCheckedCell = el; - } - })); - - }); // mark initialized this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS); } - _checkMultipleCells(firstRowIndex, lastRowIndex, columnId) { - for(let i=firstRowIndex; i<=lastRowIndex; i++) { - let cell = this._columns[columnId][i]; - if (cell.tagName !== 'TH') { - cell.querySelector(CHECKBOX_SELECTOR).checked = true; - } - } - } - _gatherColumns() { for (const rowIndex of Array(this._tableIndices.maxRow + 1).keys()) { for (const colIndex of Array(this._tableIndices.maxCol + 1).keys()) { From 337bf73067f2b98450d0388a1c064f0d2f9c456c Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 21 Sep 2021 17:17:22 +0200 Subject: [PATCH 102/143] feat(checkrange): new util checkrange --- frontend/src/utils/inputs/checkrange.js | 97 +++++++++++++++++++++++++ frontend/src/utils/inputs/inputs.js | 2 + 2 files changed, 99 insertions(+) create mode 100644 frontend/src/utils/inputs/checkrange.js diff --git a/frontend/src/utils/inputs/checkrange.js b/frontend/src/utils/inputs/checkrange.js new file mode 100644 index 000000000..c65475b3a --- /dev/null +++ b/frontend/src/utils/inputs/checkrange.js @@ -0,0 +1,97 @@ +import { Utility } from '../../core/utility'; +import { TableIndices } from '../../lib/table/table'; + + +const CHECKRANGE_INITIALIZED_CLASS = 'checkrange--initialized'; +const CHECKBOX_SELECTOR = '[type="checkbox"]'; + + +@Utility({ + selector: 'table:not([uw-no-check-all])', + }) +export class CheckRange { + _lastCheckedCell = null; + _element; + _tableIndices + _columns = new Array(); + + constructor(element) { + if(!element) { + throw new Error('Check All Utility cannot be setup without an element'); + } + + this._element = element; + + if (this._element.classList.contains(CHECKRANGE_INITIALIZED_CLASS)) + return false; + + this._tableIndices = new TableIndices(this._element); + + this._gatherColumns(); + + let checkboxColumns = this._findCheckboxColumns(); + + checkboxColumns.forEach(columnId => this._setUpShiftClickOnColumn(columnId)); + + this._element.classList.add(CHECKRANGE_INITIALIZED_CLASS); + } + + _setUpShiftClickOnColumn(columnId) { + let column = this._columns[columnId]; + column.forEach(el => el.addEventListener('click', (ev) => { + + if(ev.shiftKey && this.lastCheckedCell !== null) { + let lastClickedIndex = this._tableIndices.rowIndex(this._lastCheckedCell); + let currentCellIndex = this._tableIndices.rowIndex(el); + if(currentCellIndex > lastClickedIndex) + this._checkMultipleCells(lastClickedIndex, currentCellIndex, columnId); + else + this._checkMultipleCells(currentCellIndex, lastClickedIndex, columnId); + } else { + this._lastCheckedCell = el; + } + })); + } + + _checkMultipleCells(firstRowIndex, lastRowIndex, columnId) { + for(let i=firstRowIndex; i<=lastRowIndex; i++) { + let cell = this._columns[columnId][i]; + if (cell.tagName !== 'TH') { + cell.querySelector(CHECKBOX_SELECTOR).checked = true; + } + } + } + + + _gatherColumns() { + for (const rowIndex of Array(this._tableIndices.maxRow + 1).keys()) { + for (const colIndex of Array(this._tableIndices.maxCol + 1).keys()) { + + const cell = this._tableIndices.getCell(rowIndex, colIndex); + + if (!cell) + continue; + + if (!this._columns[colIndex]) + this._columns[colIndex] = new Array(); + + this._columns[colIndex][rowIndex] = cell; + } + } + } + + _findCheckboxColumns() { + let checkboxColumnIds = new Array(); + this._columns.forEach((col, i) => { + if (this._isCheckboxColumn(col)) { + checkboxColumnIds.push(i); + } + }); + return checkboxColumnIds; + } + + _isCheckboxColumn(col) { + return col.every(cell => cell.tagName == 'TH' || cell.querySelector(CHECKBOX_SELECTOR)) + && col.some(cell => cell.querySelector(CHECKBOX_SELECTOR)); + } +} \ No newline at end of file diff --git a/frontend/src/utils/inputs/inputs.js b/frontend/src/utils/inputs/inputs.js index a072c2196..13d241895 100644 --- a/frontend/src/utils/inputs/inputs.js +++ b/frontend/src/utils/inputs/inputs.js @@ -2,6 +2,7 @@ import { Checkbox } from './checkbox'; import { FileInput } from './file-input'; import { FileMaxSize } from './file-max-size'; import { Password } from './password'; +import { CheckRange } from './checkrange'; import './inputs.sass'; import './radio-group.sass'; @@ -11,4 +12,5 @@ export const InputUtils = [ FileInput, FileMaxSize, Password, + CheckRange, ]; From ce6f09dd857f53dc8c350d7d29b2164c78645b59 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 28 Sep 2021 16:47:03 +0200 Subject: [PATCH 103/143] feat(checkrange): added tooltip --- frontend/src/utils/inputs/checkrange.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/frontend/src/utils/inputs/checkrange.js b/frontend/src/utils/inputs/checkrange.js index c65475b3a..76f80260a 100644 --- a/frontend/src/utils/inputs/checkrange.js +++ b/frontend/src/utils/inputs/checkrange.js @@ -38,6 +38,7 @@ export class CheckRange { _setUpShiftClickOnColumn(columnId) { let column = this._columns[columnId]; + this._addToolTip(column[0]); column.forEach(el => el.addEventListener('click', (ev) => { if(ev.shiftKey && this.lastCheckedCell !== null) { @@ -53,6 +54,24 @@ export class CheckRange { })); } + _addToolTip(cell){ + console.log('adding Tooltip'); + let tooltipWrap = document.createElement('span'); + tooltipWrap.className = 'tooltip'; + + let tooltipContent = document.createElement('span'); + tooltipContent.className = 'tooltip__content'; + tooltipContent.appendChild(document.createTextNode('Shift Click to mark multiple cells.')); + tooltipWrap.append(tooltipContent); + + let tooltipHandle = document.createElement('span'); + tooltipHandle.className = 'tooltip__handle'; + tooltipWrap.append(tooltipHandle); + + let firstChild = cell.firstChild; + firstChild.parentNode.insertBefore(tooltipWrap, firstChild); + } + _checkMultipleCells(firstRowIndex, lastRowIndex, columnId) { for(let i=firstRowIndex; i<=lastRowIndex; i++) { let cell = this._columns[columnId][i]; From ed752b01c04d8e8958072b13c3ad300836b1a598 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 28 Sep 2021 17:29:21 +0200 Subject: [PATCH 104/143] chore(tooltips): implemented a library for frontend tooltips --- .../src/lib/tooltips/frontend-tooltips.js | 20 ++++++++++++++++++ frontend/src/utils/inputs/checkrange.js | 21 ++----------------- 2 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 frontend/src/lib/tooltips/frontend-tooltips.js diff --git a/frontend/src/lib/tooltips/frontend-tooltips.js b/frontend/src/lib/tooltips/frontend-tooltips.js new file mode 100644 index 000000000..e81ece714 --- /dev/null +++ b/frontend/src/lib/tooltips/frontend-tooltips.js @@ -0,0 +1,20 @@ +export class FrontendTooltips { + + static addToolTip(element, text) { + console.log('adding Tooltip'); + let tooltipWrap = document.createElement('span'); + tooltipWrap.className = 'tooltip'; + + let tooltipContent = document.createElement('span'); + tooltipContent.className = 'tooltip__content'; + tooltipContent.appendChild(document.createTextNode(text)); + tooltipWrap.append(tooltipContent); + + let tooltipHandle = document.createElement('span'); + tooltipHandle.className = 'tooltip__handle'; + tooltipWrap.append(tooltipHandle); + + let firstChild = element.firstChild; + firstChild.parentNode.insertBefore(tooltipWrap, firstChild); + } +} \ No newline at end of file diff --git a/frontend/src/utils/inputs/checkrange.js b/frontend/src/utils/inputs/checkrange.js index 76f80260a..18f5ee2ff 100644 --- a/frontend/src/utils/inputs/checkrange.js +++ b/frontend/src/utils/inputs/checkrange.js @@ -1,5 +1,6 @@ import { Utility } from '../../core/utility'; import { TableIndices } from '../../lib/table/table'; +import { FrontendTooltips } from '../../lib/tooltips/frontend-tooltips'; const CHECKRANGE_INITIALIZED_CLASS = 'checkrange--initialized'; @@ -38,7 +39,7 @@ export class CheckRange { _setUpShiftClickOnColumn(columnId) { let column = this._columns[columnId]; - this._addToolTip(column[0]); + FrontendTooltips.addToolTip(column[0], 'Shift Click to mark multiple cells'); column.forEach(el => el.addEventListener('click', (ev) => { if(ev.shiftKey && this.lastCheckedCell !== null) { @@ -54,24 +55,6 @@ export class CheckRange { })); } - _addToolTip(cell){ - console.log('adding Tooltip'); - let tooltipWrap = document.createElement('span'); - tooltipWrap.className = 'tooltip'; - - let tooltipContent = document.createElement('span'); - tooltipContent.className = 'tooltip__content'; - tooltipContent.appendChild(document.createTextNode('Shift Click to mark multiple cells.')); - tooltipWrap.append(tooltipContent); - - let tooltipHandle = document.createElement('span'); - tooltipHandle.className = 'tooltip__handle'; - tooltipWrap.append(tooltipHandle); - - let firstChild = cell.firstChild; - firstChild.parentNode.insertBefore(tooltipWrap, firstChild); - } - _checkMultipleCells(firstRowIndex, lastRowIndex, columnId) { for(let i=firstRowIndex; i<=lastRowIndex; i++) { let cell = this._columns[columnId][i]; From f82cf602d1af6058971a59f64afffaa55fffdbc6 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 1 Oct 2021 23:33:23 +0200 Subject: [PATCH 105/143] chore: update core team --- templates/i18n/help-instructions/de-de-formal.hamlet | 12 ++++++------ templates/i18n/help-instructions/en-eu.hamlet | 12 +++++++----- templates/i18n/imprint/de-de-formal.hamlet | 6 +++--- templates/i18n/imprint/en.hamlet | 6 +++--- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/templates/i18n/help-instructions/de-de-formal.hamlet b/templates/i18n/help-instructions/de-de-formal.hamlet index 99370c184..f9dce6a42 100644 --- a/templates/i18n/help-instructions/de-de-formal.hamlet +++ b/templates/i18n/help-instructions/de-de-formal.hamlet @@ -2,14 +2,14 @@ $newline never

        - Bitte bedenken Sie beim Stellen Ihrer Anfrage, dass das # - Uni2work-Kernteam aktuell aus Sarah Vaupel und Gregor Kleen besteht # - und zwei Personen nicht hinreichend sind um in allen Fällen eine # - zeitnahe Bearbeitung Ihres Anliegens zu garantieren. + Bitte bedenken Sie beim Stellen Ihrer Anfrage, dass das Uni2work-Kernteam aus # + Sarah Vaupel # + besteht und # + eine Person nicht hinreichend ist, # + um in allen Fällen eine zeitnahe Bearbeitung Ihres Anliegens zu garantieren.

        Falls sich Ihr Anliegen auf eine konkrete Veranstaltung bezieht, # ziehen Sie bitte auch in Betracht (insbesondere bei zeitkritischen # - Anliegen wie z.B. Abgaben) sich direkt an die Kursverwalter zu # - wenden. + Anliegen wie z.B. Abgaben) sich direkt an die Kursverwalter zu wenden. diff --git a/templates/i18n/help-instructions/en-eu.hamlet b/templates/i18n/help-instructions/en-eu.hamlet index 65e205bae..fe2b19102 100644 --- a/templates/i18n/help-instructions/en-eu.hamlet +++ b/templates/i18n/help-instructions/en-eu.hamlet @@ -2,14 +2,16 @@ $newline never

        - When formulating your request please consider that the Uni2work core # - team currently consists of Sarah Vaupel and Gregor Kleen and that # - two people are not enough to guarantee a timely answer in all cases. + When formulating your request, please consider that the Uni2work core team consists of # + Sarah Vaupel # + and that # + one person is # + not enough to guarantee a timely answer in all cases.

        If your request is related to a specific course, please also # consider contacting the relevant course administrators as well. # - Especially if your request is time sensitive (e.g. submitting for an # - exercise sheet). + Especially if your request is time sensitive (e.g. submitting for # + an exercise sheet). diff --git a/templates/i18n/imprint/de-de-formal.hamlet b/templates/i18n/imprint/de-de-formal.hamlet index d415a5780..af4c29eca 100644 --- a/templates/i18n/imprint/de-de-formal.hamlet +++ b/templates/i18n/imprint/de-de-formal.hamlet @@ -3,12 +3,12 @@ $newline never

        Inhalt
          -
        • Gregor Kleen & Sarah Vaupel +
        • Sarah Vaupel
        • Oettingenstraße 67
        • D-80538 München +
        • Raum L101
        • E-Mail: ^{mailtoHtml "uni2work@ifi.lmu.de"} -
        • Telefon (Gregor Kleen): +49 (0) 89 / 2180 - 9139 -
        • Telefon (Sarah Vaupel): — +
        • Telefon: +49 (0) 89 / 2180 - 9139

          Jugendschutz
            diff --git a/templates/i18n/imprint/en.hamlet b/templates/i18n/imprint/en.hamlet index c2506db40..a005c7396 100644 --- a/templates/i18n/imprint/en.hamlet +++ b/templates/i18n/imprint/en.hamlet @@ -3,12 +3,12 @@ $newline never

            Contents
              -
            • Gregor Kleen & Sarah Vaupel +
            • Sarah Vaupel
            • Oettingenstraße 67
            • D-80538 München (Germany) +
            • Room L101
            • E-Mail: ^{mailtoHtml "uni2work@ifi.lmu.de"} -
            • Telefon (Gregor Kleen): +49 (0) 89 / 2180 - 9139 -
            • Telefon (Sarah Vaupel): — +
            • Phone: +49 (0) 89 / 2180 - 9139

              Youth Protection
                From 1eed96014f933d6e7ead8bcb1f5ca475382906e8 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Sat, 2 Oct 2021 23:18:48 +0200 Subject: [PATCH 106/143] chore(release): 25.22.1 --- CHANGELOG.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- package.yaml | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcf5299d5..2e36d2589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.22.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.0...v25.22.1) (2021-10-02) + + +### Bug Fixes + +* **course-admins:** display course admins as admins instead of assistants ([f1fe444](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f1fe4447fbe7e96e55aaf284c7083338b5135ab6)) + ## [25.22.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.0...v25.22.0) (2021-08-30) diff --git a/package-lock.json b/package-lock.json index 517e89556..52a998cb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.0", + "version": "25.22.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2e56de4a0..71a8c4795 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.0", + "version": "25.22.1", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index 5c6556bfb..d12ae451f 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.22.0 +version: 25.22.1 dependencies: - base - yesod From 96d3b1207bd55682c17ff398a68972320bf5c711 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 13 Oct 2021 14:42:12 +0200 Subject: [PATCH 107/143] chore(workflows): bump workflows --- testdata/workflows | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/workflows b/testdata/workflows index 1a788c67f..39640b53f 160000 --- a/testdata/workflows +++ b/testdata/workflows @@ -1 +1 @@ -Subproject commit 1a788c67fe98cadf1e29b0e328072437955fd660 +Subproject commit 39640b53fb43578f35d17f7a0b6cdf7e3cdaa0bd From 9b45d007bcd26745f97cce4f5e0648db21d259cc Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 13 Oct 2021 14:59:12 +0200 Subject: [PATCH 108/143] chore(release): 25.22.2 --- CHANGELOG.md | 2 ++ package-lock.json | 2 +- package.json | 2 +- package.yaml | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e36d2589..2f742526c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.22.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.1...v25.22.2) (2021-10-13) + ## [25.22.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.0...v25.22.1) (2021-10-02) diff --git a/package-lock.json b/package-lock.json index 52a998cb5..38fe5251b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.1", + "version": "25.22.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 71a8c4795..1902d7a63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.1", + "version": "25.22.2", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index d12ae451f..32949f723 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.22.1 +version: 25.22.2 dependencies: - base - yesod From da3b3391bd5aa9990dfb2818847cf8524ee68a9d Mon Sep 17 00:00:00 2001 From: ros Date: Tue, 19 Oct 2021 14:31:26 +0200 Subject: [PATCH 109/143] feat(erweiterung such-filter usersr): first try --- src/Handler/Users.hs | 11 +++++++++++ testdata/workflows | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Handler/Users.hs b/src/Handler/Users.hs index 29963c64e..01d46fc49 100644 --- a/src/Handler/Users.hs +++ b/src/Handler/Users.hs @@ -167,6 +167,15 @@ postUsersR = do -- Set.foldr (\needle acc -> acc E.||. (user E.^. UserDisplayName) `E.hasInfix` needle) eFalse (criterion :: Set.Set Text) E.any (\c -> user E.^. UserDisplayName `E.hasInfix` E.val c) criteria ) + , ( "user-ident", FilterColumn $ \user criterion -> case getLast (criterion :: Last Text) of + Nothing -> E.val True :: E.SqlExpr (E.Value Bool) + Just needle -> (E.castString (user E.^. UserIdent) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) + ) + , ( "user-email", FilterColumn $ \user criterion -> case getLast (criterion :: Last Text) of + Nothing -> E.val True :: E.SqlExpr (E.Value Bool) + Just needle -> (E.castString (user E.^. UserEmail) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) + E.||. (E.castString (user E.^. UserDisplayEmail) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) +) , ( "matriculation", FilterColumn $ \user (criteria :: Set.Set Text) -> if | Set.null criteria -> E.true -- TODO: why can this be eFalse and work still? | otherwise -> E.any (\c -> user E.^. UserMatrikelnummer `E.hasInfix` E.val c) criteria @@ -192,6 +201,8 @@ postUsersR = do ] , dbtFilterUI = \mPrev -> mconcat [ prismAForm (singletonFilter "user-search") mPrev $ aopt textField (fslI MsgName) + , prismAForm (singletonFilter "user-ident") mPrev $ aopt textField (fslI MsgAdminUserIdent) + , prismAForm (singletonFilter "user-email") mPrev $ aopt textField (fslI MsgAdminUserEmail) -- , prismAForm (singletonFilter "matriculation" ) mPrev $ aopt textField (fslI MsgTableMatrikelNr) , prismAForm (singletonFilter "matriculation") mPrev $ aopt matriculationField (fslI MsgTableMatrikelNr) , prismAForm (singletonFilter "auth-ldap" . maybePrism _PathPiece) mPrev $ aopt (lift `hoistField` selectFieldList [(MsgAuthPWHash "", False), (MsgAuthLDAP, True)]) (fslI MsgAuthMode) diff --git a/testdata/workflows b/testdata/workflows index 39640b53f..071c245fb 160000 --- a/testdata/workflows +++ b/testdata/workflows @@ -1 +1 @@ -Subproject commit 39640b53fb43578f35d17f7a0b6cdf7e3cdaa0bd +Subproject commit 071c245fbdd7d409f83627dbd705ac0d10a22d4f From 61c773f51cddb65dd0529f17799cbf7871023137 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 19 Oct 2021 20:51:07 +0200 Subject: [PATCH 110/143] feat(messages): added frontend translation class --- frontend/src/messages.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 frontend/src/messages.js diff --git a/frontend/src/messages.js b/frontend/src/messages.js new file mode 100644 index 000000000..1f1ce3574 --- /dev/null +++ b/frontend/src/messages.js @@ -0,0 +1,17 @@ +export class Translations { + static translations = { + 'checkrangeTooltip' : { + 'de' : 'Shift-Klick, um mehrere Zellen zu markieren.', + 'en' : 'Shift-click to mark multiple cells.', + }, + }; + + static getTranslation(key, language) { + let json = Translations.translations[key]; + if(language === 'en') { + return json.en; + } else { + return json.de; + } + } +}; \ No newline at end of file From e74b61065a5de811bd411c0e863fddf9b9baada0 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Tue, 19 Oct 2021 20:52:00 +0200 Subject: [PATCH 111/143] feat(tooltips): added translatable tooltip --- frontend/src/lib/tooltips/frontend-tooltips.js | 1 - frontend/src/utils/inputs/checkrange.js | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/tooltips/frontend-tooltips.js b/frontend/src/lib/tooltips/frontend-tooltips.js index e81ece714..a77c9d4a0 100644 --- a/frontend/src/lib/tooltips/frontend-tooltips.js +++ b/frontend/src/lib/tooltips/frontend-tooltips.js @@ -1,7 +1,6 @@ export class FrontendTooltips { static addToolTip(element, text) { - console.log('adding Tooltip'); let tooltipWrap = document.createElement('span'); tooltipWrap.className = 'tooltip'; diff --git a/frontend/src/utils/inputs/checkrange.js b/frontend/src/utils/inputs/checkrange.js index 18f5ee2ff..2abf5d582 100644 --- a/frontend/src/utils/inputs/checkrange.js +++ b/frontend/src/utils/inputs/checkrange.js @@ -1,6 +1,7 @@ import { Utility } from '../../core/utility'; import { TableIndices } from '../../lib/table/table'; import { FrontendTooltips } from '../../lib/tooltips/frontend-tooltips'; +import { Translations } from '../../messages'; const CHECKRANGE_INITIALIZED_CLASS = 'checkrange--initialized'; @@ -39,7 +40,9 @@ export class CheckRange { _setUpShiftClickOnColumn(columnId) { let column = this._columns[columnId]; - FrontendTooltips.addToolTip(column[0], 'Shift Click to mark multiple cells'); + let language = document.documentElement.lang; + let toolTipMessage = Translations.getTranslation('checkrangeTooltip', language); + FrontendTooltips.addToolTip(column[0], toolTipMessage); column.forEach(el => el.addEventListener('click', (ev) => { if(ev.shiftKey && this.lastCheckedCell !== null) { From adf9709567d9a320f2c17d3c5dde940c2f9d8862 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 21 Oct 2021 14:58:08 +0200 Subject: [PATCH 112/143] fix(navigation): always link workflows nav to instances --- src/Foundation/Navigation.hs | 87 +++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/src/Foundation/Navigation.hs b/src/Foundation/Navigation.hs index 28303797b..a1133b8e3 100644 --- a/src/Foundation/Navigation.hs +++ b/src/Foundation/Navigation.hs @@ -573,8 +573,8 @@ navLinkAccess NavLink{..} = case navAccess' of defaultLinks :: ( MonadHandler m , HandlerSite m ~ UniWorX - , MonadThrow m - , WithRunDB SqlReadBackend (HandlerFor UniWorX) m + -- , MonadThrow m + -- , WithRunDB SqlReadBackend (HandlerFor UniWorX) m , BearerAuthSite UniWorX ) => m [Nav] defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header. @@ -761,12 +761,14 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the , do guardVolatile clusterVolatileWorkflowsEnabled - authCtx <- getAuthContext - (haveInstances, haveWorkflows) <- lift . memcachedBy (Just . Right $ 2 * diffMinute) (NavCacheHaveTopWorkflowsInstances authCtx) . useRunDB $ (,) - <$> haveTopWorkflowInstances - <*> haveTopWorkflowWorkflows + -- authCtx <- getAuthContext + -- (haveInstances, haveWorkflows) <- lift . memcachedBy (Just . Right $ 2 * diffMinute) (NavCacheHaveTopWorkflowsInstances authCtx) . useRunDB $ (,) + -- <$> haveTopWorkflowInstances + -- <*> haveTopWorkflowWorkflows - if | haveInstances -> return NavHeader + mUserId <- maybeAuthId + -- if | haveInstances -> return NavHeader + if | isJust mUserId -> return NavHeader { navHeaderRole = NavHeaderPrimary , navIcon = IconMenuWorkflows , navLink = NavLink @@ -778,18 +780,18 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the , navForceActive = False } } - | haveWorkflows -> return NavHeader - { navHeaderRole = NavHeaderPrimary - , navIcon = IconMenuWorkflows - , navLink = NavLink - { navLabel = MsgMenuTopWorkflowWorkflowListHeader - , navRoute = TopWorkflowWorkflowListR - , navAccess' = NavAccessTrue - , navType = NavTypeLink { navModal = False } - , navQuick' = mempty - , navForceActive = False - } - } + -- | haveWorkflows -> return NavHeader + -- { navHeaderRole = NavHeaderPrimary + -- , navIcon = IconMenuWorkflows + -- , navLink = NavLink + -- { navLabel = MsgMenuTopWorkflowWorkflowListHeader + -- , navRoute = TopWorkflowWorkflowListR + -- , navAccess' = NavAccessTrue + -- , navType = NavTypeLink { navModal = False } + -- , navQuick' = mempty + -- , navForceActive = False + -- } + -- } | otherwise -> mzero , return NavHeaderContainer { navHeaderRole = NavHeaderPrimary @@ -2730,34 +2732,35 @@ haveWorkflowWorkflows rScope = hoist liftHandler . withReaderT (projectBackend @ lift $ anyM roles evalRole -haveTopWorkflowInstances, haveTopWorkflowWorkflows +-- haveTopWorkflowInstances, +haveTopWorkflowWorkflows :: ( MonadHandler m, HandlerSite m ~ UniWorX , BackendCompatible SqlReadBackend backend , BearerAuthSite UniWorX ) => ReaderT backend m Bool -haveTopWorkflowInstances = hoist liftHandler . withReaderT (projectBackend @SqlReadBackend) . $cachedHere . maybeT (return False) $ do - roles <- memcachedBy @(Set ((RouteWorkflowScope, WorkflowInstanceName), WorkflowRole UserId)) (Just $ Right diffDay) NavCacheHaveTopWorkflowInstancesRoles $ do - let - getInstances = E.selectSource . E.from $ \workflowInstance -> do - E.where_ . isTopWorkflowScopeSql $ workflowInstance E.^. WorkflowInstanceScope - return workflowInstance - instanceRoles (Entity _ WorkflowInstance{..}) = do - rScope <- toRouteWorkflowScope $ _DBWorkflowScope # workflowInstanceScope - wiGraph <- lift $ getSharedIdWorkflowGraph workflowInstanceGraph - return . Set.mapMonotonic ((rScope, workflowInstanceName), ) . fold $ do - WGN{..} <- wiGraph ^.. _wgNodes . folded - WorkflowGraphEdgeInitial{..} <- wgnEdges ^.. folded - return wgeActors - runConduit $ transPipe lift getInstances .| C.foldMapM instanceRoles - - let - evalRole :: _ -> ReaderT SqlReadBackend (HandlerFor UniWorX) Bool - evalRole ((rScope, win), role) = do - let route = _WorkflowScopeRoute # (rScope, WorkflowInstanceR win WIInitiateR) - is _Authorized <$> hasWorkflowRole Nothing role route False - - lift $ anyM roles evalRole +-- haveTopWorkflowInstances = hoist liftHandler . withReaderT (projectBackend @SqlReadBackend) . $cachedHere . maybeT (return False) $ do +-- roles <- memcachedBy @(Set ((RouteWorkflowScope, WorkflowInstanceName), WorkflowRole UserId)) (Just $ Right diffDay) NavCacheHaveTopWorkflowInstancesRoles $ do +-- let +-- getInstances = E.selectSource . E.from $ \workflowInstance -> do +-- E.where_ . isTopWorkflowScopeSql $ workflowInstance E.^. WorkflowInstanceScope +-- return workflowInstance +-- instanceRoles (Entity _ WorkflowInstance{..}) = do +-- rScope <- toRouteWorkflowScope $ _DBWorkflowScope # workflowInstanceScope +-- wiGraph <- lift $ getSharedIdWorkflowGraph workflowInstanceGraph +-- return . Set.mapMonotonic ((rScope, workflowInstanceName), ) . fold $ do +-- WGN{..} <- wiGraph ^.. _wgNodes . folded +-- WorkflowGraphEdgeInitial{..} <- wgnEdges ^.. folded +-- return wgeActors +-- runConduit $ transPipe lift getInstances .| C.foldMapM instanceRoles +-- +-- let +-- evalRole :: _ -> ReaderT SqlReadBackend (HandlerFor UniWorX) Bool +-- evalRole ((rScope, win), role) = do +-- let route = _WorkflowScopeRoute # (rScope, WorkflowInstanceR win WIInitiateR) +-- is _Authorized <$> hasWorkflowRole Nothing role route False +-- +-- lift $ anyM roles evalRole haveTopWorkflowWorkflows = hoist liftHandler . withReaderT (projectBackend @SqlReadBackend) . $cachedHere . maybeT (return False) $ do roles <- memcachedBy (Just $ Right diffDay) NavCacheHaveTopWorkflowWorkflowsRoles $ do let From 9f939ba805b89197a7cc898d47c747bc456e75c0 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 21 Oct 2021 15:12:24 +0200 Subject: [PATCH 113/143] chore(release): 25.22.3 --- CHANGELOG.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- package.yaml | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f742526c..58dfb13b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.22.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.2...v25.22.3) (2021-10-21) + + +### Bug Fixes + +* **navigation:** always link workflows nav to instances ([adf9709](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/adf9709567d9a320f2c17d3c5dde940c2f9d8862)) + ## [25.22.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.1...v25.22.2) (2021-10-13) ## [25.22.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.0...v25.22.1) (2021-10-02) diff --git a/package-lock.json b/package-lock.json index 38fe5251b..78874c4d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.2", + "version": "25.22.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1902d7a63..a93525c62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.2", + "version": "25.22.3", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index 32949f723..c3c7d0ae7 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.22.2 +version: 25.22.3 dependencies: - base - yesod From b580503c1afe626b03b2bea1fdd2468bbf1bc125 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Mon, 25 Oct 2021 22:26:10 +0200 Subject: [PATCH 114/143] chore(backend): added testdata/workflows directory --- testdata/workflows | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/workflows b/testdata/workflows index 1a788c67f..cf7dcf58c 160000 --- a/testdata/workflows +++ b/testdata/workflows @@ -1 +1 @@ -Subproject commit 1a788c67fe98cadf1e29b0e328072437955fd660 +Subproject commit cf7dcf58c524176bbdd27ff279d68a5ab90cd06e From 29c54db06f01659a3a6419009964a85cd11d5441 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Oct 2021 22:47:03 +0200 Subject: [PATCH 115/143] fix(routes): make access to workflows free --- routes | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes b/routes index c7299e84c..8051d646f 100644 --- a/routes +++ b/routes @@ -78,7 +78,7 @@ /global-workflows/instances/#WorkflowInstanceName GlobalWorkflowInstanceR: /edit GWIEditR GET POST /delete GWIDeleteR GET POST - /workflows GWIWorkflowsR GET !¬empty + /workflows GWIWorkflowsR GET !free /initiate GWIInitiateR GET POST !workflow /update GWIUpdateR POST /global-workflows GlobalWorkflowWorkflowListR GET !free @@ -145,7 +145,7 @@ /workflows/instances/#WorkflowInstanceName SchoolWorkflowInstanceR: /edit SWIEditR GET POST /delete SWIDeleteR GET POST - /workflows SWIWorkflowsR GET !¬empty + /workflows SWIWorkflowsR GET !free /initiate SWIInitiateR GET POST !workflow /update SWIUpdateR POST /workflows SchoolWorkflowWorkflowListR GET !free From b33b50ba0e16fdffb66817f619d845428334ed57 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 26 Oct 2021 23:32:24 +0200 Subject: [PATCH 116/143] chore(release): 25.22.4 --- CHANGELOG.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- package.yaml | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58dfb13b0..5e9f80e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.22.4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.3...v25.22.4) (2021-10-26) + + +### Bug Fixes + +* **routes:** make access to workflows free ([29c54db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/29c54db06f01659a3a6419009964a85cd11d5441)) + ## [25.22.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.2...v25.22.3) (2021-10-21) diff --git a/package-lock.json b/package-lock.json index 78874c4d4..3113011ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.3", + "version": "25.22.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a93525c62..ae47ea5a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.3", + "version": "25.22.4", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index c3c7d0ae7..43bc3cbc1 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.22.3 +version: 25.22.4 dependencies: - base - yesod From 05eda7c50a5a0c61b6c3058653b8b8017ad51005 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 3 Nov 2021 13:02:29 +0100 Subject: [PATCH 117/143] Apply 3 suggestion(s) to 2 file(s) --- frontend/src/utils/inputs/checkrange.js | 3 ++- testdata/workflows | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/inputs/checkrange.js b/frontend/src/utils/inputs/checkrange.js index 2abf5d582..061b4c975 100644 --- a/frontend/src/utils/inputs/checkrange.js +++ b/frontend/src/utils/inputs/checkrange.js @@ -19,7 +19,7 @@ export class CheckRange { constructor(element) { if(!element) { - throw new Error('Check All Utility cannot be setup without an element'); + throw new Error('Check Range Utility cannot be setup without an element'); } this._element = element; @@ -39,6 +39,7 @@ export class CheckRange { } _setUpShiftClickOnColumn(columnId) { + if (!this._columns || columnId < 0 || columnId >= this._columns.length) return; let column = this._columns[columnId]; let language = document.documentElement.lang; let toolTipMessage = Translations.getTranslation('checkrangeTooltip', language); diff --git a/testdata/workflows b/testdata/workflows index cf7dcf58c..c7301b6ed 160000 --- a/testdata/workflows +++ b/testdata/workflows @@ -1 +1 @@ -Subproject commit cf7dcf58c524176bbdd27ff279d68a5ab90cd06e +Subproject commit c7301b6ed58b53be21199e4493cf791e7e91c4fd From 86ee2fb14c05e3b6a78c6c51bf961b6c41d3e5c5 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 3 Nov 2021 14:23:49 +0100 Subject: [PATCH 118/143] fix(frontend-tooltips): icon is shown --- frontend/src/lib/tooltips/frontend-tooltips.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/lib/tooltips/frontend-tooltips.js b/frontend/src/lib/tooltips/frontend-tooltips.js index a77c9d4a0..334c9f78b 100644 --- a/frontend/src/lib/tooltips/frontend-tooltips.js +++ b/frontend/src/lib/tooltips/frontend-tooltips.js @@ -11,6 +11,11 @@ export class FrontendTooltips { let tooltipHandle = document.createElement('span'); tooltipHandle.className = 'tooltip__handle'; + let icon = document.createElement('i'); + icon.classList.add('fas'); + icon.classList.add('fa-question-circle'); + tooltipHandle.append(icon); + console.log(tooltipHandle.innerHTML); tooltipWrap.append(tooltipHandle); let firstChild = element.firstChild; From b7073760eb995e09da3f626794f20b987840bd72 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 3 Nov 2021 18:55:40 +0100 Subject: [PATCH 119/143] chore(frontend-tooltips): icon after element --- frontend/src/lib/tooltips/frontend-tooltips.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/tooltips/frontend-tooltips.js b/frontend/src/lib/tooltips/frontend-tooltips.js index 334c9f78b..75535ddae 100644 --- a/frontend/src/lib/tooltips/frontend-tooltips.js +++ b/frontend/src/lib/tooltips/frontend-tooltips.js @@ -15,10 +15,8 @@ export class FrontendTooltips { icon.classList.add('fas'); icon.classList.add('fa-question-circle'); tooltipHandle.append(icon); - console.log(tooltipHandle.innerHTML); tooltipWrap.append(tooltipHandle); - - let firstChild = element.firstChild; - firstChild.parentNode.insertBefore(tooltipWrap, firstChild); + + element.append(tooltipWrap); } } \ No newline at end of file From 154f2e35cc0e154ff80002b2e0aff3a76afa1ed6 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 3 Nov 2021 19:05:13 +0100 Subject: [PATCH 120/143] feat(checkrange): unchecking a range is possible --- frontend/src/utils/inputs/checkrange.js | 26 +++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/inputs/checkrange.js b/frontend/src/utils/inputs/checkrange.js index 061b4c975..ee6441b95 100644 --- a/frontend/src/utils/inputs/checkrange.js +++ b/frontend/src/utils/inputs/checkrange.js @@ -49,16 +49,25 @@ export class CheckRange { if(ev.shiftKey && this.lastCheckedCell !== null) { let lastClickedIndex = this._tableIndices.rowIndex(this._lastCheckedCell); let currentCellIndex = this._tableIndices.rowIndex(el); + let cell = this._columns[columnId][currentCellIndex]; if(currentCellIndex > lastClickedIndex) - this._checkMultipleCells(lastClickedIndex, currentCellIndex, columnId); + this._handleCellsInBetween(cell, lastClickedIndex, currentCellIndex, columnId); else - this._checkMultipleCells(currentCellIndex, lastClickedIndex, columnId); + this._handleCellsInBetween(cell, currentCellIndex, lastClickedIndex, columnId); } else { this._lastCheckedCell = el; } })); } + _handleCellsInBetween(cell, firstRowIndex, lastRowIndex, columnId) { + if(this._isChecked(cell)) { + this._uncheckMultipleCells(firstRowIndex, lastRowIndex, columnId); + } else { + this._checkMultipleCells(firstRowIndex, lastRowIndex, columnId); + } + } + _checkMultipleCells(firstRowIndex, lastRowIndex, columnId) { for(let i=firstRowIndex; i<=lastRowIndex; i++) { let cell = this._columns[columnId][i]; @@ -68,6 +77,19 @@ export class CheckRange { } } + _uncheckMultipleCells(firstRowIndex, lastRowIndex, columnId) { + for(let i=firstRowIndex; i<=lastRowIndex; i++) { + let cell = this._columns[columnId][i]; + if (cell.tagName !== 'TH') { + cell.querySelector(CHECKBOX_SELECTOR).checked = false; + } + } + } + + _isChecked(cell) { + return cell.querySelector(CHECKBOX_SELECTOR).checked; + } + _gatherColumns() { for (const rowIndex of Array(this._tableIndices.maxRow + 1).keys()) { From 028d1016cbbe7d8e859789edcacbdef072064d8f Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 17 Nov 2021 17:18:13 +0100 Subject: [PATCH 121/143] chore: update workflows --- testdata/workflows | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/workflows b/testdata/workflows index c7301b6ed..cf7dcf58c 160000 --- a/testdata/workflows +++ b/testdata/workflows @@ -1 +1 @@ -Subproject commit c7301b6ed58b53be21199e4493cf791e7e91c4fd +Subproject commit cf7dcf58c524176bbdd27ff279d68a5ab90cd06e From d72a937d0f2d139e8c22b2c5bba59e6ae77ca1a6 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 17 Nov 2021 20:32:39 +0100 Subject: [PATCH 122/143] chore: bump workflows --- testdata/workflows | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/workflows b/testdata/workflows index 39640b53f..d567d2957 160000 --- a/testdata/workflows +++ b/testdata/workflows @@ -1 +1 @@ -Subproject commit 39640b53fb43578f35d17f7a0b6cdf7e3cdaa0bd +Subproject commit d567d2957cd2a53fb79d2b60e650236509ffe726 From 52c4206527a8a25e4a81cd7dbbc13a879a39e988 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 18 Nov 2021 19:55:08 +0100 Subject: [PATCH 123/143] chore: bump workflows --- testdata/workflows | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/workflows b/testdata/workflows index cf7dcf58c..d567d2957 160000 --- a/testdata/workflows +++ b/testdata/workflows @@ -1 +1 @@ -Subproject commit cf7dcf58c524176bbdd27ff279d68a5ab90cd06e +Subproject commit d567d2957cd2a53fb79d2b60e650236509ffe726 From 8511a052742f8dcefed1471831d75a936d1f0557 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 24 Nov 2021 22:16:50 +0100 Subject: [PATCH 124/143] chore: bump workflows --- testdata/workflows | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/workflows b/testdata/workflows index 071c245fb..d567d2957 160000 --- a/testdata/workflows +++ b/testdata/workflows @@ -1 +1 @@ -Subproject commit 071c245fbdd7d409f83627dbd705ac0d10a22d4f +Subproject commit d567d2957cd2a53fb79d2b60e650236509ffe726 From 53dab90810675f743ece284883da9c4c0e84270e Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Sun, 28 Nov 2021 15:11:42 +0100 Subject: [PATCH 125/143] fix(modal): modals are never destroyed --- frontend/src/utils/modal/modal.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/frontend/src/utils/modal/modal.js b/frontend/src/utils/modal/modal.js index 8f013572f..6b8e28a17 100644 --- a/frontend/src/utils/modal/modal.js +++ b/frontend/src/utils/modal/modal.js @@ -72,16 +72,7 @@ export class Modal { } destroy() { - this._eventManager.cleanUp(); - if (this._closerElement !== undefined) - this._closerElement.remove(); - if(this._triggerElement !== undefined) - this._triggerElement.classList.remove(MODAL_TRIGGER_CLASS); - if(this._modalsWrapper !== undefined) - this._modalsWrapper.remove(); - if(this._modalOverlay !== undefined) - this._modalOverlay.remove(); - this._element.classList.remove(MODAL_INITIALIZED_CLASS, MODAL_CLASS); + throw new Error('Destroying modals is not possible.'); } _ensureModalWrapper() { @@ -164,7 +155,6 @@ export class Modal { this._modalsWrapper.classList.remove(MODALS_WRAPPER_OPEN_CLASS); document.removeEventListener('keyup', this._onKeyUp); - this._app.utilRegistry.destroyAll(this._element); }; _fillModal(url) { From 984c0673e92697405bb8a2917d9d6835c9021e9f Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Sun, 28 Nov 2021 15:12:42 +0100 Subject: [PATCH 126/143] chore(navigate-away-prompt): add check if parent element contain a closed modal --- frontend/src/utils/form/navigate-away-prompt.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/form/navigate-away-prompt.js b/frontend/src/utils/form/navigate-away-prompt.js index da900ba72..69c430853 100644 --- a/frontend/src/utils/form/navigate-away-prompt.js +++ b/frontend/src/utils/form/navigate-away-prompt.js @@ -107,7 +107,7 @@ export class NavigateAwayPrompt { // allow the event to happen if the form was not touched by the // user (i.e. if the current FormData is equal to the initial FormData) // or the unload event was initiated by a form submit - if (!formDataHasChanged || this.unloadDueToSubmit) + if (!formDataHasChanged || this.unloadDueToSubmit || this._parentModalIsClosed()) return; // cancel the unload event. This is the standard to force the prompt to appear. @@ -117,4 +117,13 @@ export class NavigateAwayPrompt { // for all non standard compliant browsers we return a truthy value to activate the prompt. return true; } + + _parentModalIsClosed() { + const parentModal = this._element.closest('.modal'); + if (!parentModal) + return false; + + const modalClosed = !parentModal.classList.contains('modal--open'); + return modalClosed; + } } From 7dbe1ac08aacbda3b145a0da394706273dd6c639 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Sun, 28 Nov 2021 15:11:42 +0100 Subject: [PATCH 127/143] fix(modal): modals are never destroyed --- frontend/src/utils/modal/modal.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/frontend/src/utils/modal/modal.js b/frontend/src/utils/modal/modal.js index 8f013572f..6b8e28a17 100644 --- a/frontend/src/utils/modal/modal.js +++ b/frontend/src/utils/modal/modal.js @@ -72,16 +72,7 @@ export class Modal { } destroy() { - this._eventManager.cleanUp(); - if (this._closerElement !== undefined) - this._closerElement.remove(); - if(this._triggerElement !== undefined) - this._triggerElement.classList.remove(MODAL_TRIGGER_CLASS); - if(this._modalsWrapper !== undefined) - this._modalsWrapper.remove(); - if(this._modalOverlay !== undefined) - this._modalOverlay.remove(); - this._element.classList.remove(MODAL_INITIALIZED_CLASS, MODAL_CLASS); + throw new Error('Destroying modals is not possible.'); } _ensureModalWrapper() { @@ -164,7 +155,6 @@ export class Modal { this._modalsWrapper.classList.remove(MODALS_WRAPPER_OPEN_CLASS); document.removeEventListener('keyup', this._onKeyUp); - this._app.utilRegistry.destroyAll(this._element); }; _fillModal(url) { From cceef60cb84b86593b573f43d055501f484881b8 Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Sun, 28 Nov 2021 15:12:42 +0100 Subject: [PATCH 128/143] chore(navigate-away-prompt): add check if parent element contain a closed modal --- frontend/src/utils/form/navigate-away-prompt.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/form/navigate-away-prompt.js b/frontend/src/utils/form/navigate-away-prompt.js index da900ba72..69c430853 100644 --- a/frontend/src/utils/form/navigate-away-prompt.js +++ b/frontend/src/utils/form/navigate-away-prompt.js @@ -107,7 +107,7 @@ export class NavigateAwayPrompt { // allow the event to happen if the form was not touched by the // user (i.e. if the current FormData is equal to the initial FormData) // or the unload event was initiated by a form submit - if (!formDataHasChanged || this.unloadDueToSubmit) + if (!formDataHasChanged || this.unloadDueToSubmit || this._parentModalIsClosed()) return; // cancel the unload event. This is the standard to force the prompt to appear. @@ -117,4 +117,13 @@ export class NavigateAwayPrompt { // for all non standard compliant browsers we return a truthy value to activate the prompt. return true; } + + _parentModalIsClosed() { + const parentModal = this._element.closest('.modal'); + if (!parentModal) + return false; + + const modalClosed = !parentModal.classList.contains('modal--open'); + return modalClosed; + } } From 02ce82e9d2026730fd4716a2c0b070c38a6fc53f Mon Sep 17 00:00:00 2001 From: Johannes Eder Date: Wed, 8 Dec 2021 18:58:07 +0100 Subject: [PATCH 129/143] fix(check-all): correct constructor argument --- frontend/src/utils/check-all/check-all.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/utils/check-all/check-all.js b/frontend/src/utils/check-all/check-all.js index 29e33cff8..83c3a5070 100644 --- a/frontend/src/utils/check-all/check-all.js +++ b/frontend/src/utils/check-all/check-all.js @@ -45,7 +45,7 @@ export class CheckAll { let checkboxColumns = this._findCheckboxColumns(); - checkboxColumns.forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId]))); + checkboxColumns.forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId], this._eventManager))); // mark initialized this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS); From 123e199b2b77fec0149daa8b27cc1353a5cd564e Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 14 Dec 2021 20:57:59 +0100 Subject: [PATCH 130/143] chore: hlint --- src/Handler/Users.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Handler/Users.hs b/src/Handler/Users.hs index 01d46fc49..fd9d5823d 100644 --- a/src/Handler/Users.hs +++ b/src/Handler/Users.hs @@ -169,7 +169,7 @@ postUsersR = do ) , ( "user-ident", FilterColumn $ \user criterion -> case getLast (criterion :: Last Text) of Nothing -> E.val True :: E.SqlExpr (E.Value Bool) - Just needle -> (E.castString (user E.^. UserIdent) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) + Just needle -> E.castString (user E.^. UserIdent) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%) ) , ( "user-email", FilterColumn $ \user criterion -> case getLast (criterion :: Last Text) of Nothing -> E.val True :: E.SqlExpr (E.Value Bool) From 03da1f56e437152747f91551089b3767c50c1f45 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 14 Dec 2021 21:11:28 +0100 Subject: [PATCH 131/143] chore(release): 25.23.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- package.yaml | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e9f80e91..229828133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.23.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.4...v25.23.0) (2021-12-14) + + +### Features + +* **check-all:** added shift click functionality ([da1c8b5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da1c8b54510ee1436fefe97ba32372a08299b83e)) +* **checkrange:** added tooltip ([ce6f09d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce6f09dd857f53dc8c350d7d29b2164c78645b59)) +* **checkrange:** new util checkrange ([337bf73](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/337bf73067f2b98450d0388a1c064f0d2f9c456c)) +* **checkrange:** unchecking a range is possible ([154f2e3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/154f2e35cc0e154ff80002b2e0aff3a76afa1ed6)) +* **erweiterung such-filter usersr:** first try ([da3b339](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da3b3391bd5aa9990dfb2818847cf8524ee68a9d)) +* **messages:** added frontend translation class ([61c773f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/61c773f51cddb65dd0529f17799cbf7871023137)) +* **tooltips:** added translatable tooltip ([e74b610](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e74b61065a5de811bd411c0e863fddf9b9baada0)) + + +### Bug Fixes + +* **check-all:** correct constructor argument ([02ce82e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02ce82e9d2026730fd4716a2c0b070c38a6fc53f)) +* **frontend-tooltips:** icon is shown ([86ee2fb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/86ee2fb14c05e3b6a78c6c51bf961b6c41d3e5c5)) +* **modal:** modals are never destroyed ([7dbe1ac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7dbe1ac08aacbda3b145a0da394706273dd6c639)) +* **modal:** modals are never destroyed ([53dab90](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/53dab90810675f743ece284883da9c4c0e84270e)) + ## [25.22.4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.3...v25.22.4) (2021-10-26) diff --git a/package-lock.json b/package-lock.json index 3113011ef..ca26270d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.4", + "version": "25.23.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ae47ea5a3..3d6db225e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.22.4", + "version": "25.23.0", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index 43bc3cbc1..42b7b6d1d 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.22.4 +version: 25.23.0 dependencies: - base - yesod From cb00de7960c91d87f5f8fb7ecb29dd15cb61a5a3 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 28 Dec 2021 20:42:32 +0100 Subject: [PATCH 132/143] feat(course): study modules as new course property --- .../courses/courses/de-de-formal.msg | 2 + .../categories/courses/courses/en-eu.msg | 2 + messages/uniworx/utils/utils/de-de-formal.msg | 4 +- messages/uniworx/utils/utils/en-eu.msg | 4 +- models/courses.model | 1 + src/Handler/Course/Edit.hs | 60 ++++++++++--------- src/Handler/Utils/Form.hs | 4 ++ src/Model/Types.hs | 1 + src/Model/Types/StudyModules.hs | 29 +++++++++ test/Database/Fill.hs | 9 +++ 10 files changed, 86 insertions(+), 30 deletions(-) create mode 100644 src/Model/Types/StudyModules.hs diff --git a/messages/uniworx/categories/courses/courses/de-de-formal.msg b/messages/uniworx/categories/courses/courses/de-de-formal.msg index 2e1880882..6bb2edfc1 100644 --- a/messages/uniworx/categories/courses/courses/de-de-formal.msg +++ b/messages/uniworx/categories/courses/courses/de-de-formal.msg @@ -40,6 +40,8 @@ CourseDescriptionPlaceholder: Bitte mindestens die Modulbeschreibung angeben CourseHomepageExternal: Externe Homepage CourseSemesterMultipleTip: Es stehen für Sie aktuell mehrere Semester zur Auswahl. Stellen Sie bitte sicher, dass Sie das für den Kurs korrekte Semester wählen. CourseHomepageExternalPlaceholder: Optionale externe URL +CourseStudyModules: Anrechenbare Module +CourseStudyModulesTip: Komma-separierte Liste an Modulen, für welche sich Studierende diesen Kurs anrechnen lassen können. Bitte nach Möglichkeit Modulbezeichnung (z.B. WP1) und Studienordnung (z.B. Bachelor Informatik Hauptfach) angeben. CourseVisibleFrom: Sichtbar ab CourseVisibleTo: Sichtbar bis CourseVisibleFromTip: Ab diesem Zeitpunkt ist der Kurs für andere Nutzer:innen sichtbar. Ohne Datum ist der Kurs nie für andere Nutzer:innen sichtbar. Dozierende, Assistent:innen, Tutor:innen, Korrektor:innen, angemeldete Teilnehmer:innen sowie Bewerber:innen dieses Kurses sind nicht betroffen. Nimmt der Kurs an einer Zentralanmeldung teil wird die Kurssichtbarkeit während der Bewerbungsphase forciert. diff --git a/messages/uniworx/categories/courses/courses/en-eu.msg b/messages/uniworx/categories/courses/courses/en-eu.msg index c4eda4efc..21a3981f5 100644 --- a/messages/uniworx/categories/courses/courses/en-eu.msg +++ b/messages/uniworx/categories/courses/courses/en-eu.msg @@ -39,6 +39,8 @@ CourseSemester: Semester CourseDescriptionPlaceholder: Please include the module description CourseHomepageExternalPlaceholder: Optional external URL CourseHomepageExternal: External homepage +CourseStudyModules: Accountable study modules +CourseStudyModulesTip: Comma-separated list of study modules for which students may account this course. If possible, please specify module identifier (e.g. WP1) and study regulation (e.g. Bachelor Informatics Major). CourseSemesterMultipleTip: You are currently allowed to select from among multiple semesters. Please ensure that you select the appropriate semester for your course. CourseVisibleFrom: Visible from CourseVisibleTo: Visible to diff --git a/messages/uniworx/utils/utils/de-de-formal.msg b/messages/uniworx/utils/utils/de-de-formal.msg index 1464f36ae..4ccb9cc9e 100644 --- a/messages/uniworx/utils/utils/de-de-formal.msg +++ b/messages/uniworx/utils/utils/de-de-formal.msg @@ -139,4 +139,6 @@ SheetGradingPassPoints': Bestehen nach Punkten SheetGradingPassBinary': Bestanden/Nicht bestanden SheetGradingPassAlways': Automatisch bestanden, sobald korrigiert SheetTypeNormal !ident-ok: Normal -SheetTypeBonus !ident-ok: Bonus \ No newline at end of file +SheetTypeBonus !ident-ok: Bonus + +StudyModulesEmpty: Liste von anrechenbaren Modulen darf nicht leer sein \ No newline at end of file diff --git a/messages/uniworx/utils/utils/en-eu.msg b/messages/uniworx/utils/utils/en-eu.msg index 1539fdf4c..9ce640e24 100644 --- a/messages/uniworx/utils/utils/en-eu.msg +++ b/messages/uniworx/utils/utils/en-eu.msg @@ -139,4 +139,6 @@ SheetGradingPassPoints': Passing by points SheetGradingPassBinary': Pass/Fail SheetGradingPassAlways': Automatically passed when corrected SheetTypeNormal: Normal -SheetTypeBonus: Bonus \ No newline at end of file +SheetTypeBonus: Bonus + +StudyModulesEmpty: List of accountable study modules may not be empty \ No newline at end of file diff --git a/models/courses.model b/models/courses.model index 6ea7c5a40..38923b9c0 100644 --- a/models/courses.model +++ b/models/courses.model @@ -11,6 +11,7 @@ Course -- Information about a single course; contained info is always visible shorthand (CI Text) -- practical shorthand of course name, used for identification term TermId -- semester this course is taught school SchoolId + studyModules StudyModules -- study modules this course may be credited for capacity Int Maybe -- number of allowed enrolements, if restricted -- canRegisterNow = maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo visibleFrom UTCTime Maybe default=now() -- course may be visible from a given day onwards or always hidden diff --git a/src/Handler/Course/Edit.hs b/src/Handler/Course/Edit.hs index fb426ca94..6aaff8054 100644 --- a/src/Handler/Course/Edit.hs +++ b/src/Handler/Course/Edit.hs @@ -29,29 +29,30 @@ import qualified Data.Conduit.List as C data CourseForm = CourseForm - { cfCourseId :: Maybe CourseId - , cfName :: CourseName - , cfShort :: CourseShorthand - , cfSchool :: SchoolId - , cfTerm :: TermId - , cfDesc :: Maybe StoredMarkup - , cfLink :: Maybe URI - , cfVisFrom :: Maybe UTCTime - , cfVisTo :: Maybe UTCTime - , cfMatFree :: Bool - , cfAllocation :: Maybe AllocationCourseForm + { cfCourseId :: Maybe CourseId + , cfName :: CourseName + , cfShort :: CourseShorthand + , cfSchool :: SchoolId + , cfTerm :: TermId + , cfDesc :: Maybe StoredMarkup + , cfLink :: Maybe URI + , cfStudyModules :: StudyModules + , cfVisFrom :: Maybe UTCTime + , cfVisTo :: Maybe UTCTime + , cfMatFree :: Bool + , cfAllocation :: Maybe AllocationCourseForm , cfAppRequired :: Bool , cfAppInstructions :: Maybe StoredMarkup , cfAppInstructionFiles :: Maybe FileUploads , cfAppText :: Bool , cfAppFiles :: UploadMode , cfAppRatingsVisible :: Bool - , cfCapacity :: Maybe Int - , cfSecret :: Maybe Text - , cfRegFrom :: Maybe UTCTime - , cfRegTo :: Maybe UTCTime - , cfDeRegUntil :: Maybe UTCTime - , cfLecturers :: [Either (UserEmail, Maybe LecturerType) (UserId, LecturerType)] + , cfCapacity :: Maybe Int + , cfSecret :: Maybe Text + , cfRegFrom :: Maybe UTCTime + , cfRegTo :: Maybe UTCTime + , cfDeRegUntil :: Maybe UTCTime + , cfLecturers :: [Either (UserEmail, Maybe LecturerType) (UserId, LecturerType)] } data AllocationCourseForm = AllocationCourseForm @@ -73,6 +74,7 @@ courseToForm cEnt@(Entity cid Course{..}) lecs lecInvites alloc = CourseForm , cfShort = courseShorthand , cfTerm = courseTerm , cfSchool = courseSchool + , cfStudyModules = courseStudyModules , cfCapacity = courseCapacity , cfSecret = courseRegisterSecret , cfMatFree = courseMaterialFree @@ -278,30 +280,30 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB hoist (censorM $ traverseOf _head addTip) $ optionalActionW' (bool mforcedJust mpopt mayChange) allocationForm' (fslI MsgCourseAllocationParticipate) (is _Just . cfAllocation <$> template) - -- let autoUnzipInfo = [|Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu. TODO|] - multipleSchoolsMsg <- messageI Warning MsgCourseSchoolMultipleTip multipleTermsMsg <- messageI Warning MsgCourseSemesterMultipleTip (result, widget) <- flip (renderAForm FormStandard) html $ CourseForm (cfCourseId =<< template) - <$> areq (textField & cfStrip & cfCI) (fslI MsgCourseName) (cfName <$> template) + <$> areq (textField & cfStrip & cfCI) (fslI MsgCourseName) (cfName <$> template) <*> areq (textField & cfStrip & cfCI) (fslpI MsgCourseShorthand "ProMo, LinAlg1, AlgoDat, Ana2, EiP, …" -- & addAttr "disabled" "disabled" - & setTooltip MsgCourseShorthandUnique) (cfShort <$> template) + & setTooltip MsgCourseShorthandUnique) (cfShort <$> template) <* bool (pure ()) (aformMessage multipleSchoolsMsg) (length userSchools > 1) - <*> areq (schoolFieldFor userSchools) (fslI MsgCourseSchool) (cfSchool <$> template) + <*> areq (schoolFieldFor userSchools) (fslI MsgCourseSchool) (cfSchool <$> template) <* bool (pure ()) (aformMessage multipleTermsMsg) (length userTerms > 1) - <*> areq termsField (fslI MsgCourseSemester) (cfTerm <$> template) + <*> areq termsField (fslI MsgCourseSemester) (cfTerm <$> template) <*> aopt htmlField (fslpI MsgCourseDescription (mr MsgCourseDescriptionPlaceholder)) - (cfDesc <$> template) + (cfDesc <$> template) <*> aopt urlField (fslpI MsgCourseHomepageExternal (mr MsgCourseHomepageExternalPlaceholder)) - (cfLink <$> template) + (cfLink <$> template) + <*> apopt studyModulesSimpleField (fslI MsgCourseStudyModules & setTooltip MsgCourseStudyModulesTip) + (cfStudyModules <$> template) <*> aopt utcTimeField (fslpI MsgCourseVisibleFrom (mr MsgCourseDate) - & setTooltip MsgCourseVisibleFromTip) (deepAlt (cfVisFrom <$> template) newVisFrom) + & setTooltip MsgCourseVisibleFromTip) (deepAlt (cfVisFrom <$> template) newVisFrom) <*> aopt utcTimeField (fslpI MsgCourseVisibleTo (mr MsgCourseDate) - & setTooltip MsgCourseVisibleToTip) (cfVisTo <$> template) - <*> apopt checkBoxField (fslI MsgCourseMaterialFree) (cfMatFree <$> template) + & setTooltip MsgCourseVisibleToTip) (cfVisTo <$> template) + <*> apopt checkBoxField (fslI MsgCourseMaterialFree) (cfMatFree <$> template) <* aformSection MsgCourseFormSectionRegistration <*> allocationForm <*> apopt checkBoxField (fslI MsgCourseApplicationRequired & setTooltip MsgCourseApplicationRequiredTip) (cfAppRequired <$> template) @@ -496,6 +498,7 @@ courseEditHandler miButtonAction mbCourseForm = do , courseShorthand = cfShort , courseTerm = cfTerm , courseSchool = cfSchool + , courseStudyModules = cfStudyModules , courseCapacity = cfCapacity , courseRegisterSecret = cfSecret , courseMaterialFree = cfMatFree @@ -547,6 +550,7 @@ courseEditHandler miButtonAction mbCourseForm = do , courseShorthand = cfShort , courseTerm = cfTerm -- dangerous , courseSchool = cfSchool + , courseStudyModules = cfStudyModules , courseCapacity = cfCapacity , courseRegisterSecret = cfSecret , courseMaterialFree = cfMatFree diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs index 34a372192..0c7e53638 100644 --- a/src/Handler/Utils/Form.hs +++ b/src/Handler/Utils/Form.hs @@ -2529,3 +2529,7 @@ i18nFieldW :: forall a ident handler. -> Maybe (Maybe (I18n a)) -> WForm handler (FormResult (Maybe (I18n a))) i18nFieldW strField onlyAppLanguages miButtonAction miIdent fSettings fRequired mPrev' = aFormToWForm $ i18nFieldA strField onlyAppLanguages miButtonAction miIdent fSettings fRequired mPrev' + + +studyModulesSimpleField :: Field Handler StudyModules +studyModulesSimpleField = convertField (Set.fromList . map (StudyModuleFreeModule . CI.mk) . filter (not . Text.null) . map Text.strip . Text.splitOn ",") (intercalate ", " . map (CI.original . stdModFreeModule) . Set.toList) textField diff --git a/src/Model/Types.hs b/src/Model/Types.hs index 5b5562675..92963e789 100644 --- a/src/Model/Types.hs +++ b/src/Model/Types.hs @@ -23,3 +23,4 @@ import Model.Types.Markup as Types import Model.Types.Room as Types import Model.Types.Csv as Types import Model.Types.Upload as Types +import Model.Types.StudyModules as Types diff --git a/src/Model/Types/StudyModules.hs b/src/Model/Types/StudyModules.hs new file mode 100644 index 000000000..d016b9bb4 --- /dev/null +++ b/src/Model/Types/StudyModules.hs @@ -0,0 +1,29 @@ +module Model.Types.StudyModules + where + +import Import.NoModel + + +data StudyModule + = StudyModuleModule -- full (i.e. unambiguous) study module specification + { stdModRegulation :: CI Text -- TODO: Reference StudyDegree and StudyTerms instead? + , stdModRegVersion :: UTCTime + , stdModModule :: CI Text + } + | StudyModuleFreeModule -- allows for arbitrary module specifications + { stdModFreeModule :: CI Text + } + deriving (Eq, Ord, Read, Show, Generic, Typeable) + deriving anyclass (NFData) + +deriveJSON defaultOptions + { fieldLabelModifier = camelToPathPiece' 2 + , constructorTagModifier = camelToPathPiece' 2 + } ''StudyModule + +derivePersistFieldJSON ''StudyModule + +instance Binary StudyModule + + +type StudyModules = Set StudyModule diff --git a/test/Database/Fill.hs b/test/Database/Fill.hs index b57095456..1f53dcdf5 100644 --- a/test/Database/Fill.hs +++ b/test/Database/Fill.hs @@ -660,6 +660,7 @@ fillDb = do , courseShorthand = "FFP" , courseTerm = TermKey $ seasonTerm True Summer , courseSchool = ifi + , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Informatik HF P16", "Bachelor (Medien-)Informatik HF P17", "Bachelor Medieninformatik HF P18", "Master Informatik HF WP8/WP9", "Master Medieninformatik HF P2/P6", "Master Informatik NF WP20" ] , courseCapacity = Just 20 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -813,6 +814,7 @@ fillDb = do , courseShorthand = "EIP" , courseTerm = TermKey $ seasonTerm False Winter , courseSchool = ifi + , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor (Medien-)Informatik HF P1" ] , courseCapacity = Just 20 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -839,6 +841,7 @@ fillDb = do , courseShorthand = "IXD" , courseTerm = TermKey $ seasonTerm True Summer , courseSchool = ifi + , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Medieninformatik HF WP16.1 + WP16.2" ] , courseCapacity = Just 20 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -866,6 +869,7 @@ fillDb = do , courseTerm = TermKey $ seasonTerm True Winter , courseSchool = ifi , courseCapacity = Just 30 + , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Medieninformatik HF WP16.3" ] , courseVisibleFrom = Just now , courseVisibleTo = Nothing , courseRegisterFrom = Nothing @@ -891,6 +895,7 @@ fillDb = do , courseShorthand = "ProMo" , courseTerm = TermKey $ seasonTerm True Summer , courseSchool = ifi + , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Informatik HF P4", "Bachelor Medieninformatik HF P2", "Bachelor Informatik NF WP1" ] , courseCapacity = Just 50 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -1064,6 +1069,7 @@ fillDb = do , courseShorthand = "DBS" , courseTerm = TermKey $ seasonTerm False Winter , courseSchool = ifi + , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Informatik HF P15", "Bachelor Medieninformatik HF P10", "Bachelor Informatik NF WP10" ] , courseCapacity = Just 50 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -1197,6 +1203,7 @@ fillDb = do , courseShorthand = "BS" , courseTerm = TermKey $ seasonTerm False Winter , courseSchool = ifi + , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Informatik HF P8", "Bachelor Medieninformatik HF P5", "Bachelor Informatik NF WP6" ] , courseCapacity = Just 50 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -1273,6 +1280,7 @@ fillDb = do , courseShorthand = CI.mk csh , courseTerm = TermKey $ seasonTerm False Winter , courseSchool = ifi + , courseStudyModules = Set.empty , courseCapacity = Just 50 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -1335,6 +1343,7 @@ fillDb = do , courseShorthand = CI.mk csh , courseTerm = TermKey $ seasonTerm False Winter , courseSchool = ifi + , courseStudyModules = Set.empty , courseCapacity = Just cap , courseVisibleFrom = Just now , courseVisibleTo = Nothing From dbc5e99109285d4427832820a77a6b47a8098f62 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 28 Dec 2021 21:27:04 +0100 Subject: [PATCH 133/143] feat(course): show study module on course overview page --- src/Handler/Course/Show.hs | 1 + templates/course.hamlet | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/Handler/Course/Show.hs b/src/Handler/Course/Show.hs index 1f25a0b29..a488e0d7b 100644 --- a/src/Handler/Course/Show.hs +++ b/src/Handler/Course/Show.hs @@ -14,6 +14,7 @@ import Handler.Utils.Tutorial import qualified Data.CaseInsensitive as CI import qualified Data.Map as Map +import qualified Data.Set as Set import qualified Database.Esqueleto.Legacy as E import qualified Database.Esqueleto.Utils as E diff --git a/templates/course.hamlet b/templates/course.hamlet index de6452829..9dcd69070 100644 --- a/templates/course.hamlet +++ b/templates/course.hamlet @@ -69,6 +69,20 @@ $# #{summary}
                #{descr} + $if not (Set.null (courseStudyModules course)) +
                _{MsgCourseStudyModules} +
                +
                  + $forall studyModule <- Set.toList (courseStudyModules course) +
                • + $case studyModule + $of StudyModuleModule{..} + #{CI.original stdModRegulation} # + (^{formatTimeW SelFormatDate stdModRegVersion}): # + #{CI.original stdModModule} + $of StudyModuleFreeModule{..} + #{CI.original stdModFreeModule} +
                  _{MsgTableCourseSchool}
                  #{schoolName} From bbf822d63ec8ce9e224ad8934507b46b5a9f95a7 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 28 Dec 2021 22:19:27 +0100 Subject: [PATCH 134/143] test(study-modules): add missing Arbitrary instance --- test/ModelSpec.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/ModelSpec.hs b/test/ModelSpec.hs index 66b90b480..fb383203c 100644 --- a/test/ModelSpec.hs +++ b/test/ModelSpec.hs @@ -198,6 +198,10 @@ instance Arbitrary VerpMode where arbitrary = genericArbitrary shrink = genericShrink +instance Arbitrary StudyModule where + arbitrary = genericArbitrary + shrink = genericShrink + spec :: Spec spec = do From ccf583f1dd68ede0648aa6f00b2e9a0c4555b9c4 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Tue, 28 Dec 2021 22:39:43 +0100 Subject: [PATCH 135/143] chore(release): 25.24.0 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 2 +- package.json | 2 +- package.yaml | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 229828133..042278e10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.24.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.23.0...v25.24.0) (2021-12-28) + + +### Features + +* **course:** show study module on course overview page ([dbc5e99](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dbc5e99109285d4427832820a77a6b47a8098f62)) +* **course:** study modules as new course property ([cb00de7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cb00de7960c91d87f5f8fb7ecb29dd15cb61a5a3)) + ## [25.23.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.4...v25.23.0) (2021-12-14) diff --git a/package-lock.json b/package-lock.json index ca26270d5..3c8bfcad8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.23.0", + "version": "25.24.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3d6db225e..5cfb3502f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.23.0", + "version": "25.24.0", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index 42b7b6d1d..33e0c7da9 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.23.0 +version: 25.24.0 dependencies: - base - yesod From 89fadb242037151ea792667cab85fc502b135f57 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 29 Dec 2021 01:37:43 +0100 Subject: [PATCH 136/143] fix(courses): enhanced description of study modules --- messages/uniworx/categories/courses/courses/de-de-formal.msg | 4 ++-- messages/uniworx/categories/courses/courses/en-eu.msg | 4 ++-- messages/uniworx/utils/utils/de-de-formal.msg | 4 +--- messages/uniworx/utils/utils/en-eu.msg | 4 +--- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/messages/uniworx/categories/courses/courses/de-de-formal.msg b/messages/uniworx/categories/courses/courses/de-de-formal.msg index 6bb2edfc1..63d19d345 100644 --- a/messages/uniworx/categories/courses/courses/de-de-formal.msg +++ b/messages/uniworx/categories/courses/courses/de-de-formal.msg @@ -40,8 +40,8 @@ CourseDescriptionPlaceholder: Bitte mindestens die Modulbeschreibung angeben CourseHomepageExternal: Externe Homepage CourseSemesterMultipleTip: Es stehen für Sie aktuell mehrere Semester zur Auswahl. Stellen Sie bitte sicher, dass Sie das für den Kurs korrekte Semester wählen. CourseHomepageExternalPlaceholder: Optionale externe URL -CourseStudyModules: Anrechenbare Module -CourseStudyModulesTip: Komma-separierte Liste an Modulen, für welche sich Studierende diesen Kurs anrechnen lassen können. Bitte nach Möglichkeit Modulbezeichnung (z.B. WP1) und Studienordnung (z.B. Bachelor Informatik Hauptfach) angeben. +CourseStudyModules: Assoziierte Module +CourseStudyModulesTip: Komma-separierte Liste an Modulen, für welche Leistungen dieses Kurses verbucht werden können. Bitte nach Möglichkeit die Modulbezeichnung (z.B. WP1) sowie die Studienordnung (z.B. Master Informatik Hauptfach, 08. September 2010) angeben. CourseVisibleFrom: Sichtbar ab CourseVisibleTo: Sichtbar bis CourseVisibleFromTip: Ab diesem Zeitpunkt ist der Kurs für andere Nutzer:innen sichtbar. Ohne Datum ist der Kurs nie für andere Nutzer:innen sichtbar. Dozierende, Assistent:innen, Tutor:innen, Korrektor:innen, angemeldete Teilnehmer:innen sowie Bewerber:innen dieses Kurses sind nicht betroffen. Nimmt der Kurs an einer Zentralanmeldung teil wird die Kurssichtbarkeit während der Bewerbungsphase forciert. diff --git a/messages/uniworx/categories/courses/courses/en-eu.msg b/messages/uniworx/categories/courses/courses/en-eu.msg index 21a3981f5..def2c1c51 100644 --- a/messages/uniworx/categories/courses/courses/en-eu.msg +++ b/messages/uniworx/categories/courses/courses/en-eu.msg @@ -39,8 +39,8 @@ CourseSemester: Semester CourseDescriptionPlaceholder: Please include the module description CourseHomepageExternalPlaceholder: Optional external URL CourseHomepageExternal: External homepage -CourseStudyModules: Accountable study modules -CourseStudyModulesTip: Comma-separated list of study modules for which students may account this course. If possible, please specify module identifier (e.g. WP1) and study regulation (e.g. Bachelor Informatics Major). +CourseStudyModules: Associated study modules +CourseStudyModulesTip: Comma-separated list of study modules for which results of this course may be credited. If possible, please specify the module identifier (e.g. WP1) as well as the respective study regulations (e.g. Master Computer Science Major, September 8, 2010). CourseSemesterMultipleTip: You are currently allowed to select from among multiple semesters. Please ensure that you select the appropriate semester for your course. CourseVisibleFrom: Visible from CourseVisibleTo: Visible to diff --git a/messages/uniworx/utils/utils/de-de-formal.msg b/messages/uniworx/utils/utils/de-de-formal.msg index 4ccb9cc9e..1464f36ae 100644 --- a/messages/uniworx/utils/utils/de-de-formal.msg +++ b/messages/uniworx/utils/utils/de-de-formal.msg @@ -139,6 +139,4 @@ SheetGradingPassPoints': Bestehen nach Punkten SheetGradingPassBinary': Bestanden/Nicht bestanden SheetGradingPassAlways': Automatisch bestanden, sobald korrigiert SheetTypeNormal !ident-ok: Normal -SheetTypeBonus !ident-ok: Bonus - -StudyModulesEmpty: Liste von anrechenbaren Modulen darf nicht leer sein \ No newline at end of file +SheetTypeBonus !ident-ok: Bonus \ No newline at end of file diff --git a/messages/uniworx/utils/utils/en-eu.msg b/messages/uniworx/utils/utils/en-eu.msg index 9ce640e24..1539fdf4c 100644 --- a/messages/uniworx/utils/utils/en-eu.msg +++ b/messages/uniworx/utils/utils/en-eu.msg @@ -139,6 +139,4 @@ SheetGradingPassPoints': Passing by points SheetGradingPassBinary': Pass/Fail SheetGradingPassAlways': Automatically passed when corrected SheetTypeNormal: Normal -SheetTypeBonus: Bonus - -StudyModulesEmpty: List of accountable study modules may not be empty \ No newline at end of file +SheetTypeBonus: Bonus \ No newline at end of file From ceebc4e6c976dca48ae8be39cec2b9483db8027e Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 29 Dec 2021 01:55:01 +0100 Subject: [PATCH 137/143] chore(release): 25.24.1 --- CHANGELOG.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- package.yaml | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 042278e10..12bc8bf32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.24.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.24.0...v25.24.1) (2021-12-29) + + +### Bug Fixes + +* **courses:** enhanced description of study modules ([89fadb2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89fadb242037151ea792667cab85fc502b135f57)) + ## [25.24.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.23.0...v25.24.0) (2021-12-28) diff --git a/package-lock.json b/package-lock.json index 3c8bfcad8..581a2af4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.24.0", + "version": "25.24.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5cfb3502f..859701ea1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.24.0", + "version": "25.24.1", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index 33e0c7da9..18e00df71 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.24.0 +version: 25.24.1 dependencies: - base - yesod From 89b36f2d97b5ca9f5447dbc43b9219aa6b98b910 Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 20 Jan 2022 22:51:55 +0100 Subject: [PATCH 138/143] chore(study-modules): remove deprecated study module representation --- .../courses/courses/de-de-formal.msg | 2 -- .../categories/courses/courses/en-eu.msg | 2 -- models/courses.model | 1 - src/Handler/Course/Edit.hs | 6 ---- src/Handler/Course/Show.hs | 1 - src/Handler/Utils/Form.hs | 4 --- src/Model/Types.hs | 1 - src/Model/Types/StudyModules.hs | 29 ------------------- templates/course.hamlet | 14 --------- test/Database/Fill.hs | 9 ------ 10 files changed, 69 deletions(-) delete mode 100644 src/Model/Types/StudyModules.hs diff --git a/messages/uniworx/categories/courses/courses/de-de-formal.msg b/messages/uniworx/categories/courses/courses/de-de-formal.msg index 63d19d345..2e1880882 100644 --- a/messages/uniworx/categories/courses/courses/de-de-formal.msg +++ b/messages/uniworx/categories/courses/courses/de-de-formal.msg @@ -40,8 +40,6 @@ CourseDescriptionPlaceholder: Bitte mindestens die Modulbeschreibung angeben CourseHomepageExternal: Externe Homepage CourseSemesterMultipleTip: Es stehen für Sie aktuell mehrere Semester zur Auswahl. Stellen Sie bitte sicher, dass Sie das für den Kurs korrekte Semester wählen. CourseHomepageExternalPlaceholder: Optionale externe URL -CourseStudyModules: Assoziierte Module -CourseStudyModulesTip: Komma-separierte Liste an Modulen, für welche Leistungen dieses Kurses verbucht werden können. Bitte nach Möglichkeit die Modulbezeichnung (z.B. WP1) sowie die Studienordnung (z.B. Master Informatik Hauptfach, 08. September 2010) angeben. CourseVisibleFrom: Sichtbar ab CourseVisibleTo: Sichtbar bis CourseVisibleFromTip: Ab diesem Zeitpunkt ist der Kurs für andere Nutzer:innen sichtbar. Ohne Datum ist der Kurs nie für andere Nutzer:innen sichtbar. Dozierende, Assistent:innen, Tutor:innen, Korrektor:innen, angemeldete Teilnehmer:innen sowie Bewerber:innen dieses Kurses sind nicht betroffen. Nimmt der Kurs an einer Zentralanmeldung teil wird die Kurssichtbarkeit während der Bewerbungsphase forciert. diff --git a/messages/uniworx/categories/courses/courses/en-eu.msg b/messages/uniworx/categories/courses/courses/en-eu.msg index def2c1c51..c4eda4efc 100644 --- a/messages/uniworx/categories/courses/courses/en-eu.msg +++ b/messages/uniworx/categories/courses/courses/en-eu.msg @@ -39,8 +39,6 @@ CourseSemester: Semester CourseDescriptionPlaceholder: Please include the module description CourseHomepageExternalPlaceholder: Optional external URL CourseHomepageExternal: External homepage -CourseStudyModules: Associated study modules -CourseStudyModulesTip: Comma-separated list of study modules for which results of this course may be credited. If possible, please specify the module identifier (e.g. WP1) as well as the respective study regulations (e.g. Master Computer Science Major, September 8, 2010). CourseSemesterMultipleTip: You are currently allowed to select from among multiple semesters. Please ensure that you select the appropriate semester for your course. CourseVisibleFrom: Visible from CourseVisibleTo: Visible to diff --git a/models/courses.model b/models/courses.model index 38923b9c0..6ea7c5a40 100644 --- a/models/courses.model +++ b/models/courses.model @@ -11,7 +11,6 @@ Course -- Information about a single course; contained info is always visible shorthand (CI Text) -- practical shorthand of course name, used for identification term TermId -- semester this course is taught school SchoolId - studyModules StudyModules -- study modules this course may be credited for capacity Int Maybe -- number of allowed enrolements, if restricted -- canRegisterNow = maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo visibleFrom UTCTime Maybe default=now() -- course may be visible from a given day onwards or always hidden diff --git a/src/Handler/Course/Edit.hs b/src/Handler/Course/Edit.hs index 6aaff8054..6ef1789ea 100644 --- a/src/Handler/Course/Edit.hs +++ b/src/Handler/Course/Edit.hs @@ -36,7 +36,6 @@ data CourseForm = CourseForm , cfTerm :: TermId , cfDesc :: Maybe StoredMarkup , cfLink :: Maybe URI - , cfStudyModules :: StudyModules , cfVisFrom :: Maybe UTCTime , cfVisTo :: Maybe UTCTime , cfMatFree :: Bool @@ -74,7 +73,6 @@ courseToForm cEnt@(Entity cid Course{..}) lecs lecInvites alloc = CourseForm , cfShort = courseShorthand , cfTerm = courseTerm , cfSchool = courseSchool - , cfStudyModules = courseStudyModules , cfCapacity = courseCapacity , cfSecret = courseRegisterSecret , cfMatFree = courseMaterialFree @@ -297,8 +295,6 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB (cfDesc <$> template) <*> aopt urlField (fslpI MsgCourseHomepageExternal (mr MsgCourseHomepageExternalPlaceholder)) (cfLink <$> template) - <*> apopt studyModulesSimpleField (fslI MsgCourseStudyModules & setTooltip MsgCourseStudyModulesTip) - (cfStudyModules <$> template) <*> aopt utcTimeField (fslpI MsgCourseVisibleFrom (mr MsgCourseDate) & setTooltip MsgCourseVisibleFromTip) (deepAlt (cfVisFrom <$> template) newVisFrom) <*> aopt utcTimeField (fslpI MsgCourseVisibleTo (mr MsgCourseDate) @@ -498,7 +494,6 @@ courseEditHandler miButtonAction mbCourseForm = do , courseShorthand = cfShort , courseTerm = cfTerm , courseSchool = cfSchool - , courseStudyModules = cfStudyModules , courseCapacity = cfCapacity , courseRegisterSecret = cfSecret , courseMaterialFree = cfMatFree @@ -550,7 +545,6 @@ courseEditHandler miButtonAction mbCourseForm = do , courseShorthand = cfShort , courseTerm = cfTerm -- dangerous , courseSchool = cfSchool - , courseStudyModules = cfStudyModules , courseCapacity = cfCapacity , courseRegisterSecret = cfSecret , courseMaterialFree = cfMatFree diff --git a/src/Handler/Course/Show.hs b/src/Handler/Course/Show.hs index a488e0d7b..1f25a0b29 100644 --- a/src/Handler/Course/Show.hs +++ b/src/Handler/Course/Show.hs @@ -14,7 +14,6 @@ import Handler.Utils.Tutorial import qualified Data.CaseInsensitive as CI import qualified Data.Map as Map -import qualified Data.Set as Set import qualified Database.Esqueleto.Legacy as E import qualified Database.Esqueleto.Utils as E diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs index 0c7e53638..34a372192 100644 --- a/src/Handler/Utils/Form.hs +++ b/src/Handler/Utils/Form.hs @@ -2529,7 +2529,3 @@ i18nFieldW :: forall a ident handler. -> Maybe (Maybe (I18n a)) -> WForm handler (FormResult (Maybe (I18n a))) i18nFieldW strField onlyAppLanguages miButtonAction miIdent fSettings fRequired mPrev' = aFormToWForm $ i18nFieldA strField onlyAppLanguages miButtonAction miIdent fSettings fRequired mPrev' - - -studyModulesSimpleField :: Field Handler StudyModules -studyModulesSimpleField = convertField (Set.fromList . map (StudyModuleFreeModule . CI.mk) . filter (not . Text.null) . map Text.strip . Text.splitOn ",") (intercalate ", " . map (CI.original . stdModFreeModule) . Set.toList) textField diff --git a/src/Model/Types.hs b/src/Model/Types.hs index 92963e789..5b5562675 100644 --- a/src/Model/Types.hs +++ b/src/Model/Types.hs @@ -23,4 +23,3 @@ import Model.Types.Markup as Types import Model.Types.Room as Types import Model.Types.Csv as Types import Model.Types.Upload as Types -import Model.Types.StudyModules as Types diff --git a/src/Model/Types/StudyModules.hs b/src/Model/Types/StudyModules.hs deleted file mode 100644 index d016b9bb4..000000000 --- a/src/Model/Types/StudyModules.hs +++ /dev/null @@ -1,29 +0,0 @@ -module Model.Types.StudyModules - where - -import Import.NoModel - - -data StudyModule - = StudyModuleModule -- full (i.e. unambiguous) study module specification - { stdModRegulation :: CI Text -- TODO: Reference StudyDegree and StudyTerms instead? - , stdModRegVersion :: UTCTime - , stdModModule :: CI Text - } - | StudyModuleFreeModule -- allows for arbitrary module specifications - { stdModFreeModule :: CI Text - } - deriving (Eq, Ord, Read, Show, Generic, Typeable) - deriving anyclass (NFData) - -deriveJSON defaultOptions - { fieldLabelModifier = camelToPathPiece' 2 - , constructorTagModifier = camelToPathPiece' 2 - } ''StudyModule - -derivePersistFieldJSON ''StudyModule - -instance Binary StudyModule - - -type StudyModules = Set StudyModule diff --git a/templates/course.hamlet b/templates/course.hamlet index 9dcd69070..de6452829 100644 --- a/templates/course.hamlet +++ b/templates/course.hamlet @@ -69,20 +69,6 @@ $# #{summary}
                  #{descr} - $if not (Set.null (courseStudyModules course)) -
                  _{MsgCourseStudyModules} -
                  -
                    - $forall studyModule <- Set.toList (courseStudyModules course) -
                  • - $case studyModule - $of StudyModuleModule{..} - #{CI.original stdModRegulation} # - (^{formatTimeW SelFormatDate stdModRegVersion}): # - #{CI.original stdModModule} - $of StudyModuleFreeModule{..} - #{CI.original stdModFreeModule} -
                    _{MsgTableCourseSchool}
                    #{schoolName} diff --git a/test/Database/Fill.hs b/test/Database/Fill.hs index 1f53dcdf5..b57095456 100644 --- a/test/Database/Fill.hs +++ b/test/Database/Fill.hs @@ -660,7 +660,6 @@ fillDb = do , courseShorthand = "FFP" , courseTerm = TermKey $ seasonTerm True Summer , courseSchool = ifi - , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Informatik HF P16", "Bachelor (Medien-)Informatik HF P17", "Bachelor Medieninformatik HF P18", "Master Informatik HF WP8/WP9", "Master Medieninformatik HF P2/P6", "Master Informatik NF WP20" ] , courseCapacity = Just 20 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -814,7 +813,6 @@ fillDb = do , courseShorthand = "EIP" , courseTerm = TermKey $ seasonTerm False Winter , courseSchool = ifi - , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor (Medien-)Informatik HF P1" ] , courseCapacity = Just 20 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -841,7 +839,6 @@ fillDb = do , courseShorthand = "IXD" , courseTerm = TermKey $ seasonTerm True Summer , courseSchool = ifi - , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Medieninformatik HF WP16.1 + WP16.2" ] , courseCapacity = Just 20 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -869,7 +866,6 @@ fillDb = do , courseTerm = TermKey $ seasonTerm True Winter , courseSchool = ifi , courseCapacity = Just 30 - , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Medieninformatik HF WP16.3" ] , courseVisibleFrom = Just now , courseVisibleTo = Nothing , courseRegisterFrom = Nothing @@ -895,7 +891,6 @@ fillDb = do , courseShorthand = "ProMo" , courseTerm = TermKey $ seasonTerm True Summer , courseSchool = ifi - , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Informatik HF P4", "Bachelor Medieninformatik HF P2", "Bachelor Informatik NF WP1" ] , courseCapacity = Just 50 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -1069,7 +1064,6 @@ fillDb = do , courseShorthand = "DBS" , courseTerm = TermKey $ seasonTerm False Winter , courseSchool = ifi - , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Informatik HF P15", "Bachelor Medieninformatik HF P10", "Bachelor Informatik NF WP10" ] , courseCapacity = Just 50 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -1203,7 +1197,6 @@ fillDb = do , courseShorthand = "BS" , courseTerm = TermKey $ seasonTerm False Winter , courseSchool = ifi - , courseStudyModules = Set.fromList $ (StudyModuleFreeModule . CI.mk) <$> [ "Bachelor Informatik HF P8", "Bachelor Medieninformatik HF P5", "Bachelor Informatik NF WP6" ] , courseCapacity = Just 50 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -1280,7 +1273,6 @@ fillDb = do , courseShorthand = CI.mk csh , courseTerm = TermKey $ seasonTerm False Winter , courseSchool = ifi - , courseStudyModules = Set.empty , courseCapacity = Just 50 , courseVisibleFrom = Just now , courseVisibleTo = Nothing @@ -1343,7 +1335,6 @@ fillDb = do , courseShorthand = CI.mk csh , courseTerm = TermKey $ seasonTerm False Winter , courseSchool = ifi - , courseStudyModules = Set.empty , courseCapacity = Just cap , courseVisibleFrom = Just now , courseVisibleTo = Nothing From d68588037f180083cd8d2555a586a32e854bb45a Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 21 Jan 2022 09:23:23 +0100 Subject: [PATCH 139/143] chore(study-modules): remove further mention of depr'ed rep' --- test/ModelSpec.hs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/ModelSpec.hs b/test/ModelSpec.hs index fb383203c..66b90b480 100644 --- a/test/ModelSpec.hs +++ b/test/ModelSpec.hs @@ -198,10 +198,6 @@ instance Arbitrary VerpMode where arbitrary = genericArbitrary shrink = genericShrink -instance Arbitrary StudyModule where - arbitrary = genericArbitrary - shrink = genericShrink - spec :: Spec spec = do From 5bd9ea85e8f0e4387cf47116bf42c4441bdbe8b3 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Wed, 19 Jan 2022 23:19:36 +0100 Subject: [PATCH 140/143] feat(communication): support attachments in course/tutorial comm's --- messages/uniworx/utils/utils/de-de-formal.msg | 2 ++ messages/uniworx/utils/utils/en-eu.msg | 2 ++ src/Handler/Utils/Communication.hs | 17 ++++++++------ src/Handler/Utils/Files.hs | 8 +++++++ src/Jobs/Handler/Files.hs | 18 +++++++++++---- src/Jobs/Handler/SendCourseCommunication.hs | 8 +++---- src/Jobs/Types.hs | 11 +++++---- src/Mail.hs | 23 ++++++++++++++++++- src/Model/Types.hs | 1 + src/Model/Types/Communication.hs | 21 +++++++++++++++++ src/Model/Types/File.hs | 20 ++++++++++++++-- templates/mail/courseCommunication.hamlet | 2 +- 12 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 src/Model/Types/Communication.hs diff --git a/messages/uniworx/utils/utils/de-de-formal.msg b/messages/uniworx/utils/utils/de-de-formal.msg index 1464f36ae..82a4e02f3 100644 --- a/messages/uniworx/utils/utils/de-de-formal.msg +++ b/messages/uniworx/utils/utils/de-de-formal.msg @@ -17,6 +17,8 @@ RGTutorialParticipants tutn@TutorialName: Tutorium-Teilnehmer:innen (#{tutn}) RGExamRegistered examn@ExamName: Angemeldet zur Prüfung „#{examn}“ RGSheetSubmittor shn@SheetName: Abgebende für das Übungsblatt „#{shn}“ CommSubject: Betreff +CommAttachments: Anhänge +CommAttachmentsTip: Im Allgemeinen ist es vorzuziehen Dateien, die Sie mit den Empfängern teilen möchten, als Material hochzuladen (und ggf. in der Nachricht zu verlinken). So ist die Datei für die Empfänger dauerhaft abrufbar und auch Personen, die sich z.B. erst später zum Kurs anmelden, haben Zugriff auf die Datei. CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt CommTestSuccess: Nachricht wurde zu Testzwecken nur an Sie selbst versandt diff --git a/messages/uniworx/utils/utils/en-eu.msg b/messages/uniworx/utils/utils/en-eu.msg index 1539fdf4c..28a834e93 100644 --- a/messages/uniworx/utils/utils/en-eu.msg +++ b/messages/uniworx/utils/utils/en-eu.msg @@ -17,6 +17,8 @@ RGTutorialParticipants tutn: Tutorial participants (#{tutn}) RGExamRegistered examn: Registered for exam “#{examn}” RGSheetSubmittor shn: Submitted for exercise sheet “#{shn}” CommSubject: Subject +CommAttachments: Attachments +CommAttachmentsTip: In general it is preferable to upload files as course material instead of sending them as attachments. You can then link to the material from the message. The file is then permanently accessable to the recipients and to persons that, for example, register for the Course at a later date. CommSuccess n: Message was sent to #{n} #{pluralEN n "recipient" "recipients"} CommTestSuccess: Message was sent only to yourself for testing purposes diff --git a/src/Handler/Utils/Communication.hs b/src/Handler/Utils/Communication.hs index ca32a1b71..4cfca1a04 100644 --- a/src/Handler/Utils/Communication.hs +++ b/src/Handler/Utils/Communication.hs @@ -78,16 +78,16 @@ data CommunicationRoute = CommunicationRoute data Communication = Communication { cRecipients :: Set (Either UserEmail UserId) - , cSubject :: Maybe Text - , cBody :: Html + , cContent :: CommunicationContent } +makeLenses_ ''Communication + crJobsCourseCommunication, crTestJobsCourseCommunication :: CourseId -> Communication -> ConduitT () Job (YesodDB UniWorX) () crJobsCourseCommunication jCourse Communication{..} = do jSender <- requireAuthId - let jSubject = cSubject - jMailContent = cBody + let jMailContent = cContent allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients jMailObjectUUID <- liftIO getRandom jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case @@ -99,7 +99,7 @@ crTestJobsCourseCommunication jCourse comm = do jSender <- requireAuthId MsgRenderer mr <- getMsgRenderer - let comm' = comm { cSubject = Just . mr . MsgCommCourseTestSubject . fromMaybe (mr MsgUtilCommCourseSubject) $ cSubject comm } + let comm' = comm & _cContent . _ccSubject %~ Just . mr . MsgCommCourseTestSubject . fromMaybe (mr MsgUtilCommCourseSubject) crJobsCourseCommunication jCourse comm' .| C.filter ((== Right jSender) . jRecipientEmail) @@ -209,8 +209,11 @@ commR CommunicationRoute{..} = do ((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . withButtonForm' universeF . renderAForm FormStandard $ Communication <$> recipientAForm <* aformMessage recipientsListMsg - <*> aopt textField (fslI MsgCommSubject & addAttr "uw-enter-as-tab" "") Nothing - <*> (markupOutput <$> areq htmlField (fslI MsgCommBody) Nothing) + <*> ( CommunicationContent + <$> aopt textField (fslI MsgCommSubject & addAttr "uw-enter-as-tab" "") Nothing + <*> (markupOutput <$> areq htmlField (fslI MsgCommBody) Nothing) + <*> fmap fold (aopt (convertFieldM (runConduit . (.| C.foldMap Set.singleton)) yieldMany fileFieldMultiple) (fslI MsgCommAttachments & setTooltip MsgCommAttachmentsTip) Nothing) + ) formResult commRes $ \case (comm, BtnCommunicationSend) -> do runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs diff --git a/src/Handler/Utils/Files.hs b/src/Handler/Utils/Files.hs index 83b5f7552..98b1e602e 100644 --- a/src/Handler/Utils/Files.hs +++ b/src/Handler/Utils/Files.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + module Handler.Utils.Files ( sourceFile, sourceFile' , sourceFiles, sourceFiles' @@ -9,6 +11,7 @@ module Handler.Utils.Files import Import.NoFoundation hiding (First(..)) import Foundation.Type +import Foundation.DB import Utils.Metrics import Data.Monoid (First(..)) @@ -181,6 +184,11 @@ sourceFiles' = C.map sourceFile' sourceFile' :: forall file. (HasFileReference file, YesodPersistBackend UniWorX ~ SqlBackend) => file -> DBFile sourceFile' = sourceFile . view (_FileReference . _1) + +instance (YesodMail UniWorX, YesodPersistBackend UniWorX ~ SqlBackend) => ToMailPart UniWorX FileReference where + toMailPart = toMailPart <=< liftHandler . runDBRead . withReaderT projectBackend . toPureFile . sourceFile' + + respondFileConditional :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, YesodPersistBackend UniWorX ~ SqlBackend, YesodPersistRunner UniWorX) => Maybe UTCTime -> MimeType -> FileReference diff --git a/src/Jobs/Handler/Files.hs b/src/Jobs/Handler/Files.hs index 7ab592eb1..de6787c0d 100644 --- a/src/Jobs/Handler/Files.hs +++ b/src/Jobs/Handler/Files.hs @@ -47,6 +47,9 @@ import qualified Data.Foldable as F import qualified Control.Monad.State.Class as State +import Jobs.Types +import Data.Aeson.Lens (_JSON) + dispatchJobPruneSessionFiles :: JobHandler UniWorX dispatchJobPruneSessionFiles = JobHandlerAtomicWithFinalizer act fin @@ -83,6 +86,9 @@ workflowFileReferences = Map.fromList $ over (traverse . _1) nameToPathPiece , (''WorkflowWorkflow, E.selectSource (E.from $ pure . (E.^. WorkflowWorkflowState )) .| awaitForever (mapMOf_ (typesCustom @WorkflowChildren . _fileReferenceContent . _Just) yield . E.unValue)) ] +jobFileReferences :: MonadResource m => ConduitT () FileContentReference (SqlPersistT m) () +jobFileReferences = E.selectSource (E.from $ pure . (E.^. QueuedJobContent)) .| C.mapMaybe (preview _JSON . E.unValue) .| awaitForever (mapMOf_ (typesCustom @JobChildren @Job @Job @FileContentReference @FileContentReference) yield) + dispatchJobDetectMissingFiles :: JobHandler UniWorX dispatchJobDetectMissingFiles = JobHandlerAtomicDeferrableWithFinalizer act fin @@ -103,8 +109,10 @@ dispatchJobDetectMissingFiles = JobHandlerAtomicDeferrableWithFinalizer act fin E.distinctOnOrderBy [E.asc ref] $ return ref transPipe lift (E.selectSource fileReferencesQuery) .| C.mapMaybe E.unValue .| C.mapM_ (insertRef refKind) - iforM_ workflowFileReferences $ \refKind refSource -> - transPipe (lift . withReaderT projectBackend) (refSource .| C.filterM (\ref -> not <$> exists [FileContentEntryHash ==. ref])) .| C.mapM_ (insertRef refKind) + let useRefSource refKind refSource = transPipe (lift . withReaderT projectBackend) (refSource .| C.filterM (\ref -> not <$> exists [FileContentEntryHash ==. ref])) .| C.mapM_ (insertRef refKind) + iforM_ workflowFileReferences useRefSource + useRefSource (nameToPathPiece ''Job) jobFileReferences + let allMissingDb :: Set Minio.Object allMissingDb = setOf (folded . folded . re minioFileReference) missingDb @@ -204,14 +212,16 @@ dispatchJobPruneUnreferencedFiles numIterations epoch iteration = JobHandlerAtom return $ E.any E.exists (fileReferences $ fileContentEntry E.^. FileContentEntryHash) E.where_ $ chunkIdFilter unreferencedChunkHash - let unmarkWorkflowFiles (otoList -> fRefs) = E.delete . E.from $ \fileContentChunkUnreferenced -> do + let unmarkSourceFiles (otoList -> fRefs) = E.delete . E.from $ \fileContentChunkUnreferenced -> do let unreferencedChunkHash = E.unKey $ fileContentChunkUnreferenced E.^. FileContentChunkUnreferencedHash E.where_ . E.subSelectOr . E.from $ \fileContentEntry -> do E.where_ $ fileContentEntry E.^. FileContentEntryChunkHash E.==. unreferencedChunkHash return $ fileContentEntry E.^. FileContentEntryHash `E.in_` E.valList fRefs E.where_ $ chunkIdFilter unreferencedChunkHash + unmarkRefSource refSource = runConduit $ refSource .| C.map Seq.singleton .| C.chunksOfE chunkSize .| C.mapM_ unmarkSourceFiles chunkSize = 100 - in runConduit $ sequence_ workflowFileReferences .| C.map Seq.singleton .| C.chunksOfE chunkSize .| C.mapM_ unmarkWorkflowFiles + unmarkRefSource $ sequence_ workflowFileReferences + unmarkRefSource jobFileReferences let getEntryCandidates = E.selectSource . E.from $ \fileContentEntry -> do diff --git a/src/Jobs/Handler/SendCourseCommunication.hs b/src/Jobs/Handler/SendCourseCommunication.hs index 712fd4beb..7a3433645 100644 --- a/src/Jobs/Handler/SendCourseCommunication.hs +++ b/src/Jobs/Handler/SendCourseCommunication.hs @@ -20,10 +20,9 @@ dispatchJobSendCourseCommunication :: Either UserEmail UserId -> CourseId -> UserId -> UUID - -> Maybe Text - -> Html + -> CommunicationContent -> JobHandler UniWorX -dispatchJobSendCourseCommunication jRecipientEmail jAllRecipientAddresses jCourse jSender jMailObjectUUID jSubject jMailContent = JobHandlerException $ do +dispatchJobSendCourseCommunication jRecipientEmail jAllRecipientAddresses jCourse jSender jMailObjectUUID CommunicationContent{..} = JobHandlerException $ do (sender, Course{..}) <- runDB $ (,) <$> getJust jSender <*> getJust jCourse @@ -34,8 +33,9 @@ dispatchJobSendCourseCommunication jRecipientEmail jAllRecipientAddresses jCours _mailFrom .= userAddressFrom sender addMailHeader "Cc" [st|#{mr MsgCommUndisclosedRecipients}:;|] addMailHeader "Auto-Submitted" "no" - setSubjectI . prependCourseTitle courseTerm courseSchool courseShorthand $ maybe (SomeMessage MsgCommCourseSubject) SomeMessage jSubject + setSubjectI . prependCourseTitle courseTerm courseSchool courseShorthand $ maybe (SomeMessage MsgCommCourseSubject) SomeMessage ccSubject addHtmlMarkdownAlternatives ($(ihamletFile "templates/mail/courseCommunication.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX)) + forM_ ccAttachments $ addPart' . toMailPart when (jRecipientEmail == Right jSender) $ addPart' $ do partIsAttachmentCsv MsgCommAllRecipients diff --git a/src/Jobs/Types.hs b/src/Jobs/Types.hs index 94afb6b53..9efc5df8c 100644 --- a/src/Jobs/Types.hs +++ b/src/Jobs/Types.hs @@ -44,7 +44,7 @@ import Cron (CronNextMatch(..), _MatchAsap, _MatchAt, _MatchNone) import System.Clock (getTime, Clock(Monotonic), TimeSpec) import GHC.Conc (unsafeIOToSTM) -import Data.Generics.Product.Types (Children, ChGeneric) +import Data.Generics.Product.Types (Children, ChGeneric, HasTypesCustom(..)) {-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-} @@ -67,8 +67,7 @@ data Job , jCourse :: CourseId , jSender :: UserId , jMailObjectUUID :: UUID - , jSubject :: Maybe Text - , jMailContent :: Html + , jMailContent :: CommunicationContent } | JobInvitation { jInviter :: Maybe UserId , jInvitee :: UserEmail @@ -169,10 +168,14 @@ type family ChildrenJobChildren a where ChildrenJobChildren UUID = '[] ChildrenJobChildren (Key a) = '[] ChildrenJobChildren (CI a) = '[] - ChildrenJobChildren (Set a) = '[] + ChildrenJobChildren (Set v) = '[v] ChildrenJobChildren MailContext = '[] + ChildrenJobChildren (Digest a) = '[] ChildrenJobChildren a = Children ChGeneric a + +instance (Ord b', HasTypesCustom JobChildren a' b' a b) => HasTypesCustom JobChildren (Set a') (Set b') a b where + typesCustom = iso Set.toList Set.fromList . traverse . typesCustom @JobChildren classifyJob :: Job -> String diff --git a/src/Mail.hs b/src/Mail.hs index 827467b8e..4a3d560fb 100644 --- a/src/Mail.hs +++ b/src/Mail.hs @@ -42,6 +42,7 @@ import Data.Kind (Type) import Model.Types.Languages import Model.Types.Csv +import Model.Types.File import Network.Mail.Mime hiding (addPart, addAttachment) import qualified Network.Mail.Mime as Mime (addPart) @@ -89,7 +90,7 @@ import qualified Data.Binary as Binary import "network-bsd" Network.BSD (getHostName) import Data.Time.Zones (utcTZ, utcToLocalTimeTZ, timeZoneForUTCTime) -import Data.Time.LocalTime (ZonedTime(..), TimeZone(..)) +import Data.Time.LocalTime (ZonedTime(..), TimeZone(..), utcToZonedTime, utc) import Data.Time.Format (rfc822DateFormat) import Network.HaskellNet.SMTP (SMTPConnection) @@ -123,6 +124,12 @@ import Language.Haskell.TH (nameBase) import Network.Mail.Mime.Instances() +import Data.Char (isLatin1) +import Data.Text.Lazy.Encoding (decodeUtf8') +import System.FilePath (takeFileName) +import Network.HTTP.Types.Header (hETag) +import Web.HttpApiData (ToHttpApiData(toHeader)) + makeLenses_ ''Address makeLenses_ ''Mail @@ -346,6 +353,20 @@ instance YesodMail site => ToMailPart site Html where _partEncoding .= QuotedPrintableText _partContent .= PartContent (renderMarkup html) +instance YesodMail site => ToMailPart site PureFile where + toMailPart file@File{fileTitle, fileModified} = do + _partDisposition .= AttachmentDisposition (pack $ takeFileName fileTitle) + _partType .= decodeUtf8 (mimeLookup $ pack fileTitle) + let + content :: LBS.ByteString + content = file ^. _pureFileContent . _Just + isLatin = either (const False) (all isLatin1) $ decodeUtf8' content + _partEncoding .= bool Base64 QuotedPrintableText isLatin + _partContent .= PartContent content + forM_ (file ^. _FileReference . _1 . _fileReferenceContent) $ \fRefContent -> + replaceMailHeader (CI.original hETag) . Just . decodeUtf8 . toHeader $ etagFileReference # fRefContent + replaceMailHeader (CI.original hLastModified) . Just . pack . formatTime defaultTimeLocale rfc822DateFormat $ utcToZonedTime utc fileModified + instance (ToMailPart site a, RenderMessage site msg) => ToMailPart site (Hamlet.Translate msg -> a) where type MailPartReturn site (Hamlet.Translate msg -> a) = MailPartReturn site a toMailPart act = do diff --git a/src/Model/Types.hs b/src/Model/Types.hs index 5b5562675..ac591631c 100644 --- a/src/Model/Types.hs +++ b/src/Model/Types.hs @@ -23,3 +23,4 @@ import Model.Types.Markup as Types import Model.Types.Room as Types import Model.Types.Csv as Types import Model.Types.Upload as Types +import Model.Types.Communication as Types diff --git a/src/Model/Types/Communication.hs b/src/Model/Types/Communication.hs new file mode 100644 index 000000000..b21f3e101 --- /dev/null +++ b/src/Model/Types/Communication.hs @@ -0,0 +1,21 @@ +module Model.Types.Communication + ( CommunicationContent(..), _ccSubject, _ccBody, _ccAttachments + ) where + +import Import.NoModel +import Model.Types.File + +import Utils.Lens.TH + + +data CommunicationContent = CommunicationContent + { ccSubject :: Maybe Text + , ccBody :: Html + , ccAttachments :: Set FileReference + } deriving stock (Eq, Ord, Show, Read, Generic, Typeable) + deriving anyclass (Hashable, NFData) + +deriveJSON defaultOptions + { constructorTagModifier = camelToPathPiece' 1 + } ''CommunicationContent +makeLenses_ ''CommunicationContent diff --git a/src/Model/Types/File.hs b/src/Model/Types/File.hs index 0a3819c28..fae0b9a0c 100644 --- a/src/Model/Types/File.hs +++ b/src/Model/Types/File.hs @@ -18,7 +18,24 @@ module Model.Types.File , _fieldIdent, _fieldUnpackZips, _fieldMultiple, _fieldRestrictExtensions, _fieldAdditionalFiles, _fieldMaxFileSize ) where -import Import.NoModel +import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender, derivePersistFieldJSON, Proxy(..)) +import Crypto.Hash (Digest, SHA3_512) +import Language.Haskell.TH.Syntax (Lift) +import Data.Binary (Binary) +import Crypto.Hash.Instances () +import Data.Proxy (Proxy(..)) +import Control.Lens +import Utils.HttpConditional +import Data.Binary.Instances.Time () +import Data.Time.Clock.Instances () +import Data.Aeson.TH +import Utils +import Data.Kind (Type) +import Data.Universe +import Numeric.Natural +import Network.Mime +import Control.Monad.Morph +import Data.NonNull.Instances () import Database.Persist.Sql (PersistFieldSql(..)) import Web.HttpApiData (ToHttpApiData, FromHttpApiData) @@ -204,7 +221,6 @@ instance HasFileReference FileReference where instance HasFileReference PureFile where newtype FileReferenceResidual PureFile = PureFileResidual { unPureFileResidual :: Maybe ByteString } deriving (Eq, Ord, Read, Show, Generic, Typeable) - deriving newtype (ToJSON, FromJSON) deriving anyclass (NFData) _FileReference = iso toFileReference fromFileReference diff --git a/templates/mail/courseCommunication.hamlet b/templates/mail/courseCommunication.hamlet index b63e2827e..b6e305827 100644 --- a/templates/mail/courseCommunication.hamlet +++ b/templates/mail/courseCommunication.hamlet @@ -4,4 +4,4 @@ $newline never - #{jMailContent} + #{ccBody} From b749039636c61157b5fc0bea9848ab9828ee671c Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Thu, 20 Jan 2022 00:21:15 +0100 Subject: [PATCH 141/143] feat(file-field): cumulative size limit --- config/settings.yml | 2 ++ .../courses/submission/de-de-formal.msg | 1 + .../categories/courses/submission/en-eu.msg | 2 ++ messages/uniworx/utils/utils/de-de-formal.msg | 1 + messages/uniworx/utils/utils/en-eu.msg | 1 + src/Foundation/Yesod/StaticContent.hs | 4 +-- src/Handler/Utils/Communication.hs | 12 ++++++- src/Handler/Utils/Form.hs | 33 +++++++++++-------- src/Handler/Utils/Workflow/Form.hs | 2 ++ src/Import/NoModel.hs | 5 +++ src/Model/Types/File.hs | 4 ++- src/Settings.hs | 4 +++ templates/widgets/genericFileField.hamlet | 6 +++- 13 files changed, 58 insertions(+), 19 deletions(-) diff --git a/config/settings.yml b/config/settings.yml index ff72cb3c0..535504e62 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -290,3 +290,5 @@ bot-mitigations: - only-logged-in-table-sorting volatile-cluster-settings-cache-time: 10 + +communication-attachments-max-size: 20971520 # 20MiB diff --git a/messages/uniworx/categories/courses/submission/de-de-formal.msg b/messages/uniworx/categories/courses/submission/de-de-formal.msg index 145768cc4..f2eedb946 100644 --- a/messages/uniworx/categories/courses/submission/de-de-formal.msg +++ b/messages/uniworx/categories/courses/submission/de-de-formal.msg @@ -186,6 +186,7 @@ UploadModeExtensionRestrictionTip: Komma-separiert. Wenn keine Dateiendungen ang UploadModeExtensionRestrictionMultipleTip: Einschränkung von Dateiendungen erfolgt für alle hochgeladenen Dateien, auch innerhalb von ZIP-Archiven. FileUploadMaxSize maxSize@Text: Datei darf maximal #{maxSize} groß sein FileUploadMaxSizeMultiple maxSize@Text: Dateien dürfen jeweils maximal #{maxSize} groß sein +FileUploadCumulativeMaxSize maxSize@Text: Dateien dürfen insgesamt maximal #{maxSize} groß sein InvalidPseudonym pseudonym@Text: Invalides Pseudonym "#{pseudonym}" InvalidPseudonymSubmissionIgnored oPseudonyms@Text iPseudonym@Text: Abgabe mit Pseudonymen „#{oPseudonyms}“ wurde ignoriert, da „#{iPseudonym}“ nicht automatisiert zu einem validen Pseudonym korrigiert werden konnte. diff --git a/messages/uniworx/categories/courses/submission/en-eu.msg b/messages/uniworx/categories/courses/submission/en-eu.msg index 0574c4a9d..2d0ffb872 100644 --- a/messages/uniworx/categories/courses/submission/en-eu.msg +++ b/messages/uniworx/categories/courses/submission/en-eu.msg @@ -186,6 +186,8 @@ UploadModeExtensionRestrictionTip: Comma-separated. If no file extensions are sp UploadModeExtensionRestrictionMultipleTip: Checks for valid file extension are performed for all uploaded files, including those packed within zip-archives. FileUploadMaxSize maxSize: File may be up to #{maxSize} in size FileUploadMaxSizeMultiple maxSize: Files may each be up to #{maxSize} in size +FileUploadCumulativeMaxSize maxSize: Files may be no larger than #{maxSize} in total + InvalidPseudonym pseudonym: Invalid pseudonym “#{pseudonym}” InvalidPseudonymSubmissionIgnored oPseudonyms iPseudonym: The submission with pseudonyms “#{oPseudonyms}” has been ignored since “#{iPseudonym}” could not be automatically corrected to be a valid pseudonym. PseudonymAutocorrections: Suggestions: diff --git a/messages/uniworx/utils/utils/de-de-formal.msg b/messages/uniworx/utils/utils/de-de-formal.msg index 82a4e02f3..6e80466d9 100644 --- a/messages/uniworx/utils/utils/de-de-formal.msg +++ b/messages/uniworx/utils/utils/de-de-formal.msg @@ -55,6 +55,7 @@ UploadSpecificFileMaxSizeNegative: Maximale Dateigröße darf nicht negativ sein UploadSpecificFileEmptyOk: Leere Uploads erlauben UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}" GenericFileFieldFileTooLarge file@FilePath: „#{file}“ ist zu groß +GenericFileFieldCumulativeTooLarge: Hochgeladene Dateien sind zu groß GenericFileFieldInvalidExtension file@FilePath: „#{file}” hat keine zulässige Dateiendung OnlyUploadOneFile: Bitte nur eine Datei hochladen. UploadAtLeastOneNonemptyFile: Bitte mindestens eine nichtleere Datei hochladen. diff --git a/messages/uniworx/utils/utils/en-eu.msg b/messages/uniworx/utils/utils/en-eu.msg index 28a834e93..652674005 100644 --- a/messages/uniworx/utils/utils/en-eu.msg +++ b/messages/uniworx/utils/utils/en-eu.msg @@ -55,6 +55,7 @@ UploadSpecificFileMaxSizeNegative: Maximum filesize may not be negative UploadSpecificFileEmptyOk: Allow empty uploads UnknownPseudonymWord pseudonymWord: Invalid pseudonym-word “#{pseudonymWord}” GenericFileFieldFileTooLarge file: “#{file}” is too large +GenericFileFieldCumulativeTooLarge: Uploaded files are too large GenericFileFieldInvalidExtension file: “#{file}” does not have an acceptable file extension OnlyUploadOneFile: Please only upload one file UploadAtLeastOneNonemptyFile: Please upload at least one nonempty file. diff --git a/src/Foundation/Yesod/StaticContent.hs b/src/Foundation/Yesod/StaticContent.hs index a60ace7ff..057c7b873 100644 --- a/src/Foundation/Yesod/StaticContent.hs +++ b/src/Foundation/Yesod/StaticContent.hs @@ -27,10 +27,10 @@ addStaticContent ext _mime content = do for ((,) <$> appWidgetMemcached <*> appWidgetMemcachedConf appSettings') $ \(mConn, WidgetMemcachedConf{ widgetMemcachedConf = MemcachedConf { memcachedExpiry }, widgetMemcachedBaseUrl }) -> do let expiry = maybe 0 ceiling memcachedExpiry touch = liftIO $ Memcached.touch expiry (encodeUtf8 $ pack fileName) mConn - add = liftIO $ Memcached.add zeroBits expiry (encodeUtf8 $ pack fileName) content mConn + addItem = liftIO $ Memcached.add zeroBits expiry (encodeUtf8 $ pack fileName) content mConn absoluteLink = unpack widgetMemcachedBaseUrl fileName catchIf Memcached.isKeyNotFound touch . const $ - handleIf Memcached.isKeyExists (const $ return ()) add + handleIf Memcached.isKeyExists (const $ return ()) addItem return . Left $ pack absoluteLink where -- Generate a unique filename based on the content itself, this is used diff --git a/src/Handler/Utils/Communication.hs b/src/Handler/Utils/Communication.hs index 4cfca1a04..39e1681ce 100644 --- a/src/Handler/Utils/Communication.hs +++ b/src/Handler/Utils/Communication.hs @@ -206,13 +206,23 @@ commR CommunicationRoute{..} = do recipientsListMsg <- messageI Info MsgCommRecipientsList + attachmentsMaxSize <- getsYesod $ view _appCommunicationAttachmentsMaxSize + let attachmentField = genericFileField $ return FileField + { fieldIdent = Nothing + , fieldUnpackZips = FileFieldUserOption True False + , fieldMultiple = True + , fieldRestrictExtensions = Nothing + , fieldAdditionalFiles = _FileReferenceFileReferenceTitleMap # Map.empty + , fieldMaxFileSize = Nothing, fieldMaxCumulativeSize = attachmentsMaxSize + , fieldAllEmptyOk = True + } ((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . withButtonForm' universeF . renderAForm FormStandard $ Communication <$> recipientAForm <* aformMessage recipientsListMsg <*> ( CommunicationContent <$> aopt textField (fslI MsgCommSubject & addAttr "uw-enter-as-tab" "") Nothing <*> (markupOutput <$> areq htmlField (fslI MsgCommBody) Nothing) - <*> fmap fold (aopt (convertFieldM (runConduit . (.| C.foldMap Set.singleton)) yieldMany fileFieldMultiple) (fslI MsgCommAttachments & setTooltip MsgCommAttachmentsTip) Nothing) + <*> fmap fold (aopt (convertFieldM (runConduit . (.| C.foldMap Set.singleton)) yieldMany attachmentField) (fslI MsgCommAttachments & setTooltip MsgCommAttachmentsTip) Nothing) ) formResult commRes $ \case (comm, BtnCommunicationSend) -> do diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs index 34a372192..92940d471 100644 --- a/src/Handler/Utils/Form.hs +++ b/src/Handler/Utils/Form.hs @@ -998,15 +998,20 @@ genericFileField mkOpts = Field{..} = not (permittedExtension opts fName) && (not doUnpack || ((/=) `on` simpleContentType) (mimeLookup fName) typeZip) - whenIsJust fieldMaxFileSize $ \maxSize -> forM_ files $ \fInfo -> do - fLength <- runConduit $ fileSource fInfo .| C.takeE (fromIntegral $ succ maxSize) .| C.lengthE - when (fLength > maxSize) $ do - when (is _Just mIdent) $ - liftHandler . runDB . runConduit $ - mapM_ (transPipe lift . handleFile) files - .| handleUpload opts mIdent - .| C.sinkNull - throwE . SomeMessage . MsgGenericFileFieldFileTooLarge . unpack $ fileName fInfo + whenIsJust (ignoreNothing min fieldMaxFileSize fieldMaxCumulativeSize) $ \takeSize -> + flip evalAccumT mempty . forM_ files $ \fInfo -> do + fLength <- lift . runConduit $ fileSource fInfo .| C.takeE (fromIntegral $ succ takeSize) .| C.lengthE + add $ Sum fLength + Sum cummSize <- look + when (NTop (Just cummSize) > NTop fieldMaxCumulativeSize || NTop (Just fLength) > NTop fieldMaxFileSize) $ do + when (is _Just mIdent) $ + lift . liftHandler . runDB . runConduit $ + mapM_ (transPipe lift . handleFile) files + .| handleUpload opts mIdent + .| C.sinkNull + when (NTop (Just fLength) > NTop fieldMaxFileSize) $ do + lift . throwE . SomeMessage . MsgGenericFileFieldFileTooLarge . unpack $ fileName fInfo + lift . throwE $ SomeMessage MsgGenericFileFieldCumulativeTooLarge if | invExt : _ <- filter invalidUploadExtension uploadedFilenames -> do @@ -1125,7 +1130,7 @@ fileFieldMultiple = genericFileField $ return FileField , fieldMultiple = True , fieldRestrictExtensions = Nothing , fieldAdditionalFiles = _FileReferenceFileReferenceTitleMap # Map.empty - , fieldMaxFileSize = Nothing + , fieldMaxFileSize = Nothing, fieldMaxCumulativeSize = Nothing , fieldAllEmptyOk = True } @@ -1145,7 +1150,7 @@ singleFileField prev = genericFileField $ do [ (fileReferenceTitle, (fileReferenceContent, fileReferenceModified, FileFieldUserOption False True)) | FileReference{..} <- Set.toList permitted ] - , fieldMaxFileSize = Nothing + , fieldMaxFileSize = Nothing, fieldMaxCumulativeSize = Nothing , fieldAllEmptyOk = True } @@ -1161,7 +1166,7 @@ specificFileField UploadSpecificFile{..} mPrev = convertField (.| fixupFileTitle [ (fileReferenceTitle, (fileReferenceContent, fileReferenceModified, FileFieldUserOption False True)) | FileReference{..} <- Set.toList previous ] - , fieldMaxFileSize = specificFileMaxSize + , fieldMaxFileSize = specificFileMaxSize, fieldMaxCumulativeSize = Nothing , fieldAllEmptyOk = specificFileEmptyOk } where @@ -1189,7 +1194,7 @@ zipFileField' doUnpack permittedExtensions emptyOk mPrev = genericFileField $ do [ (fileReferenceTitle, (fileReferenceContent, fileReferenceModified, FileFieldUserOption False True)) | FileReference{..} <- Set.toList previous ] - , fieldMaxFileSize = Nothing + , fieldMaxFileSize = Nothing, fieldMaxCumulativeSize = Nothing , fieldAllEmptyOk = emptyOk } @@ -1232,7 +1237,7 @@ multiFileField mkPermitted = genericFileField $ mkField <$> mkPermitted [ (fileReferenceTitle, (fileReferenceContent, fileReferenceModified, FileFieldUserOption False True)) | FileReference{..} <- Set.toList permitted ] - , fieldMaxFileSize = Nothing + , fieldMaxFileSize = Nothing, fieldMaxCumulativeSize = Nothing , fieldAllEmptyOk = True } diff --git a/src/Handler/Utils/Workflow/Form.hs b/src/Handler/Utils/Workflow/Form.hs index 8dfc47982..0ac389ebc 100644 --- a/src/Handler/Utils/Workflow/Form.hs +++ b/src/Handler/Utils/Workflow/Form.hs @@ -70,6 +70,7 @@ instance ToJSON (FileField FileIdent) where , pure $ "multiple" JSON..= fieldMultiple , pure $ "restrict-extensions" JSON..= fieldRestrictExtensions , pure $ "max-file-size" JSON..= fieldMaxFileSize + , pure $ "max-cumulative-size" JSON..= fieldMaxCumulativeSize , pure $ "additional-files" JSON..= addFiles' ] where addFiles' = unFileIdentFileReferenceTitleMap fieldAdditionalFiles <&> \FileIdentFileReferenceTitleMapElem{..} -> JSON.object @@ -83,6 +84,7 @@ instance FromJSON (FileField FileIdent) where fieldMultiple <- o JSON..: "multiple" fieldRestrictExtensions <- o JSON..:? "restrict-extensions" fieldMaxFileSize <- o JSON..:? "max-file-size" + fieldMaxCumulativeSize <- o JSON..:? "max-cumulative-size" fieldAllEmptyOk <- o JSON..:? "all-empty-ok" JSON..!= True addFiles' <- o JSON..:? "additional-files" JSON..!= mempty fieldAdditionalFiles <- fmap FileIdentFileReferenceTitleMap . for addFiles' $ JSON.withObject "FileIdentFileReferenceTitleMapElem" $ \o' -> do diff --git a/src/Import/NoModel.hs b/src/Import/NoModel.hs index ad0ac8f97..4a75b8fe6 100644 --- a/src/Import/NoModel.hs +++ b/src/Import/NoModel.hs @@ -118,6 +118,11 @@ import Control.Monad.Trans.State as Import ( State, runState, mapState, withState , StateT(..), mapStateT, withStateT ) +import Control.Monad.Trans.Accum as Import + ( Accum, runAccum, mapAccum + , AccumT, runAccumT, execAccumT, evalAccumT, mapAccumT + , look, looks, add + ) import Control.Monad.State.Class as Import (MonadState(state)) import Control.Monad.Trans.Writer.Lazy as Import ( Writer, runWriter, mapWriter, execWriter diff --git a/src/Model/Types/File.hs b/src/Model/Types/File.hs index fae0b9a0c..2d26ae6ce 100644 --- a/src/Model/Types/File.hs +++ b/src/Model/Types/File.hs @@ -309,7 +309,7 @@ data FileField fileid = FileField , fieldUnpackZips :: FileFieldUserOption Bool , fieldMultiple :: Bool , fieldRestrictExtensions :: Maybe (NonNull (Set Extension)) - , fieldMaxFileSize :: Maybe Natural + , fieldMaxFileSize, fieldMaxCumulativeSize :: Maybe Natural , fieldAdditionalFiles :: FileReferenceTitleMap fileid (FileFieldUserOption Bool) , fieldAllEmptyOk :: Bool } @@ -327,6 +327,7 @@ instance ToJSON (FileField FileReference) where , pure $ "multiple" JSON..= fieldMultiple , pure $ "restrict-extensions" JSON..= fieldRestrictExtensions , pure $ "max-file-size" JSON..= fieldMaxFileSize + , pure $ "max-cumulative-size" JSON..= fieldMaxCumulativeSize , pure $ "additional-files" JSON..= addFiles' , pure $ "all-empty-ok" JSON..= fieldAllEmptyOk ] @@ -342,6 +343,7 @@ instance FromJSON (FileField FileReference) where fieldMultiple <- o JSON..: "multiple" fieldRestrictExtensions <- o JSON..:? "restrict-extensions" fieldMaxFileSize <- o JSON..:? "max-file-size" + fieldMaxCumulativeSize <- o JSON..:? "max-cumulative-size" fieldAllEmptyOk <- o JSON..:? "all-empty-ok" JSON..!= True addFiles' <- o JSON..:? "additional-files" JSON..!= mempty fieldAdditionalFiles <- fmap FileReferenceFileReferenceTitleMap . for addFiles' $ JSON.withObject "FileReferenceFileReferenceTitleMapElem" $ \o' -> do diff --git a/src/Settings.hs b/src/Settings.hs index c9ab18286..af10c98f4 100644 --- a/src/Settings.hs +++ b/src/Settings.hs @@ -226,6 +226,8 @@ data AppSettings = AppSettings , appVolatileClusterSettingsCacheTime :: DiffTime , appJobMaxFlush :: Maybe Natural + + , appCommunicationAttachmentsMaxSize :: Maybe Natural } deriving Show data JobMode = JobsLocal { jobsAcceptOffload :: Bool } @@ -693,6 +695,8 @@ instance FromJSON AppSettings where appJobMaxFlush <- o .:? "job-max-flush" + appCommunicationAttachmentsMaxSize <- o .:? "communication-attachments-max-size" + return AppSettings{..} where isValidARCConf ARCConf{..} = arccMaximumWeight > 0 diff --git a/templates/widgets/genericFileField.hamlet b/templates/widgets/genericFileField.hamlet index d1f6d622f..04a5581ac 100644 --- a/templates/widgets/genericFileField.hamlet +++ b/templates/widgets/genericFileField.hamlet @@ -33,7 +33,7 @@ $if not (null fileInfos)
                    _{MsgUtilAddMoreFiles} $# new files - + $if fieldMultiple
                    @@ -57,6 +57,10 @@ $maybe maxSize <- fieldMaxFileSize $else _{MsgFileUploadMaxSize (textBytes maxSize)} +$maybe maxSize <- fieldMaxCumulativeSize +
                    + _{MsgFileUploadCumulativeMaxSize (textBytes maxSize)} + $if not (fieldOptionForce fieldUnpackZips)
                    ^{iconTooltip (i18n MsgAutoUnzipInfo) Nothing False} From 89cca0f9ac5f09fddcdf32b309379ac3fb2a235a Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 21 Jan 2022 09:27:59 +0100 Subject: [PATCH 142/143] chore(changelog): communication attachments --- .../changelog/communication-attachments.de-de-formal.hamlet | 2 ++ templates/i18n/changelog/communication-attachments.en-eu.hamlet | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 templates/i18n/changelog/communication-attachments.de-de-formal.hamlet create mode 100644 templates/i18n/changelog/communication-attachments.en-eu.hamlet diff --git a/templates/i18n/changelog/communication-attachments.de-de-formal.hamlet b/templates/i18n/changelog/communication-attachments.de-de-formal.hamlet new file mode 100644 index 000000000..83b10409b --- /dev/null +++ b/templates/i18n/changelog/communication-attachments.de-de-formal.hamlet @@ -0,0 +1,2 @@ +$newline never +An Kurs- und Tutoriumsmitteilungen können nun Dateien angehängt werden. diff --git a/templates/i18n/changelog/communication-attachments.en-eu.hamlet b/templates/i18n/changelog/communication-attachments.en-eu.hamlet new file mode 100644 index 000000000..378ed8ad5 --- /dev/null +++ b/templates/i18n/changelog/communication-attachments.en-eu.hamlet @@ -0,0 +1,2 @@ +$newline never +Course- and tutorial messages (emails) may now carry attached files. From 3d09793ac69b7559ec031cf220415f1d40bd253b Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Fri, 21 Jan 2022 16:34:46 +0100 Subject: [PATCH 143/143] chore(release): 25.25.0 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 2 +- package.json | 2 +- package.yaml | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12bc8bf32..fe60e202e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.25.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.24.1...v25.25.0) (2022-01-21) + + +### Features + +* **communication:** support attachments in course/tutorial comm's ([5bd9ea8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5bd9ea85e8f0e4387cf47116bf42c4441bdbe8b3)) +* **file-field:** cumulative size limit ([b749039](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b749039636c61157b5fc0bea9848ab9828ee671c)) + ## [25.24.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.24.0...v25.24.1) (2021-12-29) diff --git a/package-lock.json b/package-lock.json index 581a2af4f..cc145124e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.24.1", + "version": "25.25.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 859701ea1..afc057afd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.24.1", + "version": "25.25.0", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index 18e00df71..2648ecc7b 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.24.1 +version: 25.25.0 dependencies: - base - yesod