fix(submissions): take care when to display corrections

Also cleanup usage of Utils via hlint
This commit is contained in:
Gregor Kleen 2021-03-18 23:12:36 +01:00
parent 1763237d5a
commit a6390eccbd
22 changed files with 150 additions and 90 deletions

View File

@ -181,3 +181,13 @@
- fixity: "infix 4 <.>=" - fixity: "infix 4 <.>="
- fixity: "infix 4 <<.>=" - fixity: "infix 4 <<.>="
- fixity: "infix 4 <<<.>=" - fixity: "infix 4 <<<.>="
- suggest: { lhs: maybeT (return ()), rhs: maybeT_ }
- warn: { lhs: length xs >= n, rhs: minLength n xs, note: IncreasesLaziness }
- warn: { lhs: n <= length xs, rhs: minLength n xs, note: IncreasesLaziness }
- warn: { lhs: length xs > n, rhs: minLength (n + 1) xs, note: IncreasesLaziness }
- warn: { lhs: n < length xs, rhs: minLength (n + 1) xs, note: IncreasesLaziness }
- warn: { lhs: length xs <= n, rhs: maxLength n xs, note: IncreasesLaziness }
- warn: { lhs: n >= length xs, rhs: maxLength n xs, note: IncreasesLaziness }
- warn: { lhs: length xs < n, rhs: maxLength (n - 1) xs, note: IncreasesLaziness }
- warn: { lhs: n > length xs, rhs: maxLength (n - 1) xs, note: IncreasesLaziness }

View File

@ -1076,11 +1076,11 @@ tagAccessPredicate AuthCourseTime = APDB $ \_ _ _mAuthId route _ -> case route o
r -> $unsupportedAuthPredicate AuthCourseTime r r -> $unsupportedAuthPredicate AuthCourseTime r
tagAccessPredicate AuthExamTime = APDB $ \_ _ _ route _ -> case route of tagAccessPredicate AuthExamTime = APDB $ \_ _ _ route _ -> case route of
CSubmissionR tid ssh csh shn _cID CorrectionR -> maybeT (unauthorizedI MsgUnauthorizedCorrectionExamTime) $ do CSubmissionR tid ssh csh shn _cID CorrectionR -> maybeT (unauthorizedI MsgUnauthorizedCorrectionExamTime) $ do
cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh cid <- MaybeT . $cachedHereBinary (tid, ssh, csh) . getKeyBy $ TermSchoolCourseShort tid ssh csh
Entity _sid Sheet{..} <- $cachedHereBinary (cid, shn) . MaybeT . getBy $ CourseSheet cid shn Entity _sid Sheet{..} <- MaybeT . $cachedHereBinary (cid, shn) . getBy $ CourseSheet cid shn
whenIsJust (sheetType ^? _examPart . from _SqlKey) $ \epId -> do whenIsJust (sheetType ^? _examPart . from _SqlKey) $ \epId -> do
ExamPart{examPartExam} <- $cachedHereBinary epId . MaybeT $ get epId ExamPart{examPartExam} <- MaybeT . $cachedHereBinary epId $ get epId
Exam{..} <- $cachedHereBinary examPartExam . MaybeT $ get examPartExam Exam{..} <- MaybeT . $cachedHereBinary examPartExam $ get examPartExam
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
guard $ NTop (Just now) >= NTop examFinished guard $ NTop (Just now) >= NTop examFinished
return Authorized return Authorized

View File

@ -61,7 +61,7 @@ postAAddUserR tid ssh ash = do
unless (courseApplicationCourse `Map.member` aauApplications) $ unless (courseApplicationCourse `Map.member` aauApplications) $
audit $ TransactionCourseApplicationDeleted courseApplicationCourse courseApplicationUser appId audit $ TransactionCourseApplicationDeleted courseApplicationCourse courseApplicationUser appId
iforM_ aauApplications $ \cId ApplicationForm{..} -> maybeT (return ()) $ do iforM_ aauApplications $ \cId ApplicationForm{..} -> maybeT_ $ do
prio <- hoistMaybe afPriority prio <- hoistMaybe afPriority
let rated = afRatingVeto || is _Just afRatingPoints let rated = afRatingVeto || is _Just afRatingPoints
appId <- lift $ insert CourseApplication appId <- lift $ insert CourseApplication

View File

@ -81,7 +81,7 @@ postEECorrectR tid ssh coursen examn = do
, GuessUserSurname (ident :: UserSurname) , GuessUserSurname (ident :: UserSurname)
, GuessUserFirstName (ident :: UserFirstName) , GuessUserFirstName (ident :: UserFirstName)
] ]
in maybe [] (either NonEmpty.toList pure) <$> lift (guessUser pdnf $ Just $ maxCountUserMatches+1) in maybe [] (either NonEmpty.toList pure) <$> lift (guessUser pdnf . Just $ succ maxCountUserMatches)
if if
| is _Nothing ciqResults, is _Nothing ciqGrade -> do | is _Nothing ciqResults, is _Nothing ciqGrade -> do

View File

@ -52,8 +52,11 @@ postCorrectionR tid ssh csh shn cid = do
ur <- getUrlRenderParams ur <- getUrlRenderParams
tr <- getTranslate tr <- getTranslate
case results of case results of
[(Entity cId Course{}, Entity shId Sheet{..}, Entity _ subm@Submission{..}, corrector, E.Value filesCorrected)] -> do [(Entity cId Course{}, Entity shId Sheet{..}, subEnt@(Entity _ subm@Submission{..}), corrector, E.Value filesCorrected)] -> do
sheetTypeDesc <- runDB $ sheetTypeDescription cId sheetType (sheetTypeDesc, invisibleWidget) <- runDB $ do
sheetTypeDesc <- sheetTypeDescription cId sheetType
invisibleWidget <- correctionInvisibleWidget tid ssh csh shn cid subEnt
return (sheetTypeDesc, invisibleWidget)
let ratingComment = submissionRatingComment >>= (\c -> c <$ guard (not $ null c)) . Text.strip let ratingComment = submissionRatingComment >>= (\c -> c <$ guard (not $ null c)) . Text.strip
pointsForm = case sheetType of pointsForm = case sheetType of
NotGraded NotGraded
@ -115,10 +118,6 @@ postCorrectionR tid ssh csh shn cid = do
, SubmissionRatingComment =. ratingComment' , SubmissionRatingComment =. ratingComment'
] ]
when (rated && is _Nothing submissionRatingTime) $ do
$logDebugS "CorrectionR" [st|Rated #{tshow sub}|]
queueDBJob . JobQueueNotification $ NotificationSubmissionRated sub
addMessageI Success $ if addMessageI Success $ if
| rated -> MsgRatingUpdated | rated -> MsgRatingUpdated
| is _Nothing ratingComment' | is _Nothing ratingComment'
@ -142,7 +141,7 @@ postCorrectionR tid ssh csh shn cid = do
headingWgt = [whamlet| headingWgt = [whamlet|
$newline never $newline never
_{heading} _{heading}
$if not (submissionRatingDone subm) $if is _Just invisibleWidget || not (submissionRatingDone subm)
\ ^{isVisibleWidget False} \ ^{isVisibleWidget False}
|] |]
@ -150,6 +149,7 @@ postCorrectionR tid ssh csh shn cid = do
setTitleI heading setTitleI heading
urlArchive <- toTextUrl . CSubmissionR tid ssh csh shn cid $ SubArchiveR SubmissionCorrected urlArchive <- toTextUrl . CSubmissionR tid ssh csh shn cid $ SubArchiveR SubmissionCorrected
let userCorrection = $(widgetFile "correction-user") let userCorrection = $(widgetFile "correction-user")
fromMaybe (return ()) invisibleWidget
$(widgetFile "correction") $(widgetFile "correction")
_ -> notFound _ -> notFound
getCorrectionUserR tid ssh csh shn cid = do getCorrectionUserR tid ssh csh shn cid = do

View File

@ -17,7 +17,7 @@ import qualified Data.Conduit.Combinators as Conduit
subDownloadSource :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionFileType -> FilePath -> ConduitT () SubmissionFile (YesodDB UniWorX) () subDownloadSource :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionFileType -> FilePath -> ConduitT () SubmissionFile (YesodDB UniWorX) ()
subDownloadSource tid ssh csh shn cID (submissionFileTypeIsUpdate -> isUpdate) path = maybeT (return ()) $ do subDownloadSource tid ssh csh shn cID (submissionFileTypeIsUpdate -> isUpdate) path = maybeT_ $ do
(submissionID, isRating) <- hoist lift $ do (submissionID, isRating) <- hoist lift $ do
submissionID <- lift $ submissionMatchesSheet tid ssh csh shn cID submissionID <- lift $ submissionMatchesSheet tid ssh csh shn cID
@ -57,7 +57,7 @@ getSubDownloadR tid ssh csh shn cID sft@(submissionFileTypeIsUpdate -> isUpdate)
subArchiveSource :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionFileType -> ConduitT () (Either SubmissionFile DBFile) (YesodDB UniWorX) () subArchiveSource :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionFileType -> ConduitT () (Either SubmissionFile DBFile) (YesodDB UniWorX) ()
subArchiveSource tid ssh csh shn cID sfType = maybeT (return ()) $ do subArchiveSource tid ssh csh shn cID sfType = maybeT_ $ do
when (sfType == SubmissionCorrected) $ when (sfType == SubmissionCorrected) $
guardM . lift . lift . hasReadAccessTo $ CSubmissionR tid ssh csh shn cID CorrectionR guardM . lift . lift . hasReadAccessTo $ CSubmissionR tid ssh csh shn cID CorrectionR

View File

@ -30,14 +30,6 @@ import Data.Aeson.Lens
import Handler.Submission.SubmissionUserInvite import Handler.Submission.SubmissionUserInvite
data CorrectionInvisibleReason
= CorrectionInvisibleExamUnfinished
| CorrectionInvisibleRatingNotDone
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)
embedRenderMessage ''UniWorX ''CorrectionInvisibleReason id
makeSubmissionForm :: CourseId -> Maybe SubmissionId -> UploadMode -> SheetGroup -> Bool -> Set (Either UserEmail UserId) -> Form (Maybe FileUploads, Set (Either UserEmail UserId)) makeSubmissionForm :: CourseId -> Maybe SubmissionId -> UploadMode -> SheetGroup -> Bool -> Set (Either UserEmail UserId) -> Form (Maybe FileUploads, Set (Either UserEmail UserId))
makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = identifyForm FIDsubmission . renderAForm FormStandard $ (,) makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = identifyForm FIDsubmission . renderAForm FormStandard $ (,)
<$> fileUploadForm (not isLecturer && is _Nothing msmid) (fslI . bool MsgSubmissionFile MsgSubmissionArchive) uploadMode <$> fileUploadForm (not isLecturer && is _Nothing msmid) (fslI . bool MsgSubmissionFile MsgSubmissionArchive) uploadMode
@ -484,26 +476,13 @@ submissionHelper tid ssh csh shn mcid = do
| otherwise -> redirect $ CSheetR tid ssh csh shn SShowR | otherwise -> redirect $ CSheetR tid ssh csh shn SShowR
Nothing -> return () Nothing -> return ()
(Entity _ Sheet{..}, buddies, lastEdits, maySubmit, _, _, msubmission, corrector) <- runDB getSheetInfo (Entity _ Sheet{..}, _, lastEdits, maySubmit, _, _, msubmission, corrector) <- runDB getSheetInfo
(showCorrection, correctionVisible, correctionInvisibleReasons) <- fmap (fromMaybe (False, False, Set.empty)) . for mcid $ \cid -> runDB $ do (showCorrection, correctionInvisible) <- fmap (fromMaybe (False, Nothing)) . for ((,) <$> mcid <*> (Entity <$> msmid <*> msubmission)) $ \(cid, subEnt) -> runDB $ do
showCorrection <- hasReadAccessTo $ CSubmissionR tid ssh csh shn cid CorrectionR showCorrection <- hasReadAccessTo $ CSubmissionR tid ssh csh shn cid CorrectionR
correctionVisible <- allMOf (folded . _Right) buddies $ \bId -> is _Authorized <$> evalAccessFor (Just bId) (CSubmissionR tid ssh csh shn cid CorrectionR) False correctionInvisible <- correctionInvisibleWidget tid ssh csh shn cid subEnt
correctionInvisibleReasons <- if return (showCorrection, correctionInvisible)
| correctionVisible -> return Set.empty
| otherwise -> mapReaderT execWriterT $ do
unless (maybe True submissionRatingDone msubmission) $
tellPoint CorrectionInvisibleRatingNotDone
maybeT (return ()) $ do
epId <- hoistMaybe $ sheetType ^? _examPart . from _SqlKey
ExamPart{examPartExam} <- MaybeT $ get epId
Exam{..} <- MaybeT $ get examPartExam
now <- liftIO getCurrentTime
unless (NTop (Just now) >= NTop examFinished) $
tellPoint CorrectionInvisibleExamUnfinished
return (showCorrection, correctionVisible, correctionInvisibleReasons)
-- Maybe construct a table to display uploaded archive files -- Maybe construct a table to display uploaded archive files
let colonnadeFiles :: _ -> Colonnade Sortable _ (DBCell (HandlerFor UniWorX) ()) let colonnadeFiles :: _ -> Colonnade Sortable _ (DBCell (HandlerFor UniWorX) ())
@ -589,5 +568,5 @@ submissionHelper tid ssh csh shn mcid = do
[ submissionRatingDone sub [ submissionRatingDone sub
, is _Just submissionRatingPoints, is _Just submissionRatingComment , is _Just submissionRatingPoints, is _Just submissionRatingComment
] ]
correctionVisibleWarnWidget = guardOn (is _Just msubmission && is _Just mcid && showCorrection && not correctionVisible) $ notificationWidget NotificationBroad Warning $(widgetFile "submission-correction-invisible") correctionVisibleWarnWidget = guardOnM (is _Just msubmission && is _Just mcid && showCorrection) correctionInvisible
$(widgetFile "submission") $(widgetFile "submission")

View File

@ -121,7 +121,7 @@ ensureApprootUserGeneratedMaybe'
) )
=> Maybe (ConduitT () (Either FileReference DBFile) m ()) => Maybe (ConduitT () (Either FileReference DBFile) m ())
-> m () -> m ()
ensureApprootUserGeneratedMaybe' source = maybeT (return ()) $ do ensureApprootUserGeneratedMaybe' source = maybeT_ $ do
route <- (,) <$> MaybeT getCurrentRoute <*> fmap reqGetParams getRequest route <- (,) <$> MaybeT getCurrentRoute <*> fmap reqGetParams getRequest
$logDebugS "ensureApproot" $ tshow route $logDebugS "ensureApproot" $ tshow route
rApproot <- hoistMaybe <=< lift . runMaybeT $ do rApproot <- hoistMaybe <=< lift . runMaybeT $ do

View File

@ -10,13 +10,14 @@ module Handler.Utils.Submission
, sinkSubmission, sinkMultiSubmission , sinkSubmission, sinkMultiSubmission
, submissionMatchesSheet , submissionMatchesSheet
, submissionDeleteRoute , submissionDeleteRoute
, correctionInvisibleWidget
) where ) where
import Import hiding (joinPath) import Import hiding (joinPath)
import Jobs.Queue import Jobs.Queue
import Yesod.Core.Types (HandlerContents(..)) import Yesod.Core.Types (HandlerContents(..))
import Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import Control.Monad.Trans.State (execStateT) import Control.Monad.Trans.State (execStateT)
import Control.Monad.RWS.Lazy (MonadRWS, RWST, execRWST) import Control.Monad.RWS.Lazy (MonadRWS, RWST, execRWST)
import qualified Control.Monad.Random as Rand import qualified Control.Monad.Random as Rand
@ -229,7 +230,7 @@ planSubmissions sid restriction = do
targetSubmissions' <- liftIO . unstableSortBy (comparing $ \subId -> Map.null . view _2 $ submissionData ! subId) $ Set.toList targetSubmissions targetSubmissions' <- liftIO . unstableSortBy (comparing $ \subId -> Map.null . view _2 $ submissionData ! subId) $ Set.toList targetSubmissions
(newSubmissionData, ()) <- (\act -> execRWST act oldSubmissionData targetSubmissionData) . forM_ (zip [1..] targetSubmissions') $ \(i, subId) -> do (newSubmissionData, ()) <- (\act -> execRWST act oldSubmissionData targetSubmissionData) . forM_ (zip [1..] targetSubmissions') $ \(i, subId) -> do
tutors <- gets $ view _2 . (! subId) -- :: Map UserId (Sum Natural) tutors <- State.gets $ view _2 . (! subId) -- :: Map UserId (Sum Natural)
let acceptableCorrectors let acceptableCorrectors
| correctorsByTut <- Map.filter (is _Just . view _byTutorial) $ sheetCorrectors `Map.restrictKeys` Map.keysSet tutors | correctorsByTut <- Map.filter (is _Just . view _byTutorial) $ sheetCorrectors `Map.restrictKeys` Map.keysSet tutors
, not $ null correctorsByTut , not $ null correctorsByTut
@ -395,7 +396,6 @@ submissionMultiArchive anonymous (Set.toList -> ids) = do
data SubmissionSinkState = SubmissionSinkState data SubmissionSinkState = SubmissionSinkState
{ sinkSeenRating :: Last Rating' { sinkSeenRating :: Last Rating'
, sinkSubmissionTouched :: Any , sinkSubmissionTouched :: Any
, sinkSubmissionNotifyRating :: Any
, sinkFilenames :: Set FilePath , sinkFilenames :: Set FilePath
} deriving (Show, Eq, Generic, Typeable) } deriving (Show, Eq, Generic, Typeable)
@ -554,7 +554,7 @@ sinkSubmission userId mExists isUpdate = do
sId <$ (guardFileTitles sheetSubmissionMode .| sinkSubmission' sId) sId <$ (guardFileTitles sheetSubmissionMode .| sinkSubmission' sId)
where where
tellSt = modify . mappend tellSt = State.modify . mappend
guardFileTitles :: MonadThrow m => SubmissionMode -> ConduitT SubmissionContent SubmissionContent m () guardFileTitles :: MonadThrow m => SubmissionMode -> ConduitT SubmissionContent SubmissionContent m ()
guardFileTitles SubmissionMode{..} guardFileTitles SubmissionMode{..}
@ -576,7 +576,7 @@ sinkSubmission userId mExists isUpdate = do
Left file@FileReference{..} -> do Left file@FileReference{..} -> do
$logDebugS "sinkSubmission" . tshow $ (submissionId, fileReferenceTitle) $logDebugS "sinkSubmission" . tshow $ (submissionId, fileReferenceTitle)
alreadySeen <- gets (Set.member fileReferenceTitle . sinkFilenames) alreadySeen <- State.gets (Set.member fileReferenceTitle . sinkFilenames)
when alreadySeen . throwM $ DuplicateFileTitle fileReferenceTitle when alreadySeen . throwM $ DuplicateFileTitle fileReferenceTitle
tellSt $ mempty{ sinkFilenames = Set.singleton fileReferenceTitle } tellSt $ mempty{ sinkFilenames = Set.singleton fileReferenceTitle }
@ -632,7 +632,7 @@ sinkSubmission userId mExists isUpdate = do
unless (submissionId' == submissionId) $ unless (submissionId' == submissionId) $
throwM $ ForeignRating cID throwM $ ForeignRating cID
alreadySeen <- gets $ is (_Wrapped . _Just) . sinkSeenRating alreadySeen <- State.gets $ is (_Wrapped . _Just) . sinkSeenRating
when alreadySeen $ throwM DuplicateRating when alreadySeen $ throwM DuplicateRating
submission <- lift $ getJust submissionId submission <- lift $ getJust submissionId
@ -671,8 +671,6 @@ sinkSubmission userId mExists isUpdate = do
mapM_ (throwM . RatingSubmissionException cID . RatingValidityException) $ validateRating sheetType r' mapM_ (throwM . RatingSubmissionException cID . RatingValidityException) $ validateRating sheetType r'
when (submissionRatingDone submission' && not (submissionRatingDone submission)) $
tellSt mempty { sinkSubmissionNotifyRating = Any True }
lift $ replace submissionId submission' lift $ replace submissionId submission'
sheetId <- lift getSheetId sheetId <- lift getSheetId
lift $ audit $ TransactionSubmissionEdit submissionId sheetId lift $ audit $ TransactionSubmissionEdit submissionId sheetId
@ -697,7 +695,7 @@ sinkSubmission userId mExists isUpdate = do
touchSubmission :: StateT SubmissionSinkState (YesodJobDB UniWorX) () touchSubmission :: StateT SubmissionSinkState (YesodJobDB UniWorX) ()
touchSubmission = do touchSubmission = do
alreadyTouched <- gets $ getAny . sinkSubmissionTouched alreadyTouched <- State.gets $ getAny . sinkSubmissionTouched
unless alreadyTouched $ do unless alreadyTouched $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
if if
@ -769,9 +767,6 @@ sinkSubmission userId mExists isUpdate = do
update submissionId [ SubmissionRatingTime =. Nothing, SubmissionRatingPoints =. Nothing, SubmissionRatingComment =. Nothing] update submissionId [ SubmissionRatingTime =. Nothing, SubmissionRatingPoints =. Nothing, SubmissionRatingComment =. Nothing]
sheetId <- getSheetId sheetId <- getSheetId
audit $ TransactionSubmissionEdit submissionId sheetId audit $ TransactionSubmissionEdit submissionId sheetId
| isUpdate
, getAny sinkSubmissionNotifyRating
-> queueDBJob . JobQueueNotification $ NotificationSubmissionRated submissionId
| not isUpdate | not isUpdate
, getAny sinkSubmissionTouched , getAny sinkSubmissionTouched
, is _Right mExists , is _Right mExists
@ -801,7 +796,7 @@ sinkMultiSubmission userId isUpdate = do
(YesodJobDB UniWorX) (YesodJobDB UniWorX)
() ()
feed sId val = do feed sId val = do
mSink <- gets $ Map.lookup sId mSink <- State.gets $ Map.lookup sId
sink <- case mSink of sink <- case mSink of
Just sink -> return sink Just sink -> return sink
Nothing -> do Nothing -> do
@ -816,7 +811,7 @@ sinkMultiSubmission userId isUpdate = do
sink' <- lift $ yield val ++$$ sink sink' <- lift $ yield val ++$$ sink
case sink' of case sink' of
Left _ -> error "sinkSubmission returned prematurely" Left _ -> error "sinkSubmission returned prematurely"
Right nSink -> modify $ Map.insert sId nSink Right nSink -> State.modify $ Map.insert sId nSink
(sinks, ignoredFiles) <- execRWSLC () Map.empty . awaitForever $ \case (sinks, ignoredFiles) <- execRWSLC () Map.empty . awaitForever $ \case
v@(Right (sId, _)) -> do v@(Right (sId, _)) -> do
cID <- encrypt sId cID <- encrypt sId
@ -930,3 +925,36 @@ submissionDeleteRoute drRecords = DeleteRoute
del del
} }
data CorrectionInvisibleReason
= CorrectionInvisibleExamUnfinished
| CorrectionInvisibleRatingNotDone
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)
embedRenderMessage ''UniWorX ''CorrectionInvisibleReason id
correctionInvisibleWidget :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission
-> Entity Submission
-> DB (Maybe Widget)
correctionInvisibleWidget tid ssh csh shn cID (Entity subId sub) = runMaybeT $ do
submittors <- lift . fmap (Set.fromList . fmap E.unValue) . E.select . E.from $ \submissionUser -> do
E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val subId
return $ submissionUser E.^. SubmissionUserUser
corrVisible <- lift . allM submittors $ \bId -> is _Authorized <$> evalAccessFor (Just bId) (CSubmissionR tid ssh csh shn cID CorrectionR) False
guard $ not corrVisible
reasons <- lift . mapReaderT execWriterT $ do
unless (submissionRatingDone sub) $
tellPoint @(Set _) CorrectionInvisibleRatingNotDone
maybeT_ $ do
Sheet{..} <- MaybeT . get $ submissionSheet sub
epId <- hoistMaybe $ sheetType ^? _examPart . from _SqlKey
ExamPart{examPartExam} <- MaybeT $ get epId
Exam{..} <- MaybeT $ get examPartExam
now <- liftIO getCurrentTime
unless (NTop (Just now) >= NTop examFinished) $
tellPoint CorrectionInvisibleExamUnfinished
return $ notificationWidget NotificationBroad Warning $(widgetFile "submission-correction-invisible")

View File

@ -695,7 +695,8 @@ withCsvExtraRep :: forall exportData csv sheetName r' k'.
-> exportData -> exportData
-> Maybe (DBTCsvEncode r' k' csv) -> Maybe (DBTCsvEncode r' k' csv)
-> [DBTExtraRep r' k'] -> [DBTExtraRep r' k'] -> [DBTExtraRep r' k'] -> [DBTExtraRep r' k']
withCsvExtraRep sheetName exportData mEncode = maybe id (flip snoc) (csvExtraRep FormatCsv) <> maybe id (flip snoc) (csvExtraRep FormatXlsx) withCsvExtraRep sheetName exportData mEncode = maybe id (flip snoc) (csvExtraRep FormatCsv)
. maybe id (flip snoc) (csvExtraRep FormatXlsx)
where where
csvExtraRep fmt = do csvExtraRep fmt = do
DBTCsvEncode{ dbtCsvNoExportData = (_ :: Maybe (AnIso' exportData' ())), .. } <- mEncode DBTCsvEncode{ dbtCsvNoExportData = (_ :: Maybe (AnIso' exportData' ())), .. } <- mEncode

View File

@ -644,7 +644,7 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
in runConduit $ getExamCorrectors .| C.mapM_ upsertExamCorrector in runConduit $ getExamCorrectors .| C.mapM_ upsertExamCorrector
let getQueuedJobs = selectSource [] [] let getQueuedJobs = selectSource [] []
updateQueuedJob (Entity jId QueuedJob{..}) = maybeT (return ()) $ do updateQueuedJob (Entity jId QueuedJob{..}) = maybeT_ $ do
(content' :: Job) <- hoistMaybe $ JSON.parseMaybe parseJSON queuedJobContent (content' :: Job) <- hoistMaybe $ JSON.parseMaybe parseJSON queuedJobContent
let uContent' = set (typesUsing @JobChildren . filtered (== oldUserId)) newUserId content' let uContent' = set (typesUsing @JobChildren . filtered (== oldUserId)) newUserId content'
guard $ uContent' /= content' guard $ uContent' /= content'

View File

@ -371,7 +371,7 @@ workflowWorkflowList (title, heading) WWListColumns{..} sqlPred = do
) )
=> WorkflowActionInfo FileReference UserId => WorkflowActionInfo FileReference UserId
-> WriterT (Maybe (Last (CryptoUUIDWorkflowStateIndex, Maybe WorkflowGraphNodeLabel, Maybe JsonWorkflowUser, UTCTime, Map WorkflowPayloadLabel JsonWorkflowPayload))) (SqlPersistT m) () -> WriterT (Maybe (Last (CryptoUUIDWorkflowStateIndex, Maybe WorkflowGraphNodeLabel, Maybe JsonWorkflowUser, UTCTime, Map WorkflowPayloadLabel JsonWorkflowPayload))) (SqlPersistT m) ()
go WorkflowActionInfo{ waiIx = stIx, waiHistory = (workflowStateCurrentPayloads -> currentPayload), waiAction = WorkflowAction{..}} = maybeT (return ()) $ do go WorkflowActionInfo{ waiIx = stIx, waiHistory = (workflowStateCurrentPayloads -> currentPayload), waiAction = WorkflowAction{..}} = maybeT_ $ do
stCID <- encryptWorkflowStateIndex wwId stIx stCID <- encryptWorkflowStateIndex wwId stIx
rScope <- hoistMaybe $ res ^. resultRouteScope rScope <- hoistMaybe $ res ^. resultRouteScope

View File

@ -121,7 +121,7 @@ workflowR rScope cID = do
) )
=> WorkflowActionInfo FileReference UserId => WorkflowActionInfo FileReference UserId
-> RWST () (Maybe (Last WorkflowCurrentState), [WorkflowHistoryItem]) (Map WorkflowPayloadLabel (Set (WorkflowFieldPayloadW FileReference UserId))) (SqlPersistT m) () -> RWST () (Maybe (Last WorkflowCurrentState), [WorkflowHistoryItem]) (Map WorkflowPayloadLabel (Set (WorkflowFieldPayloadW FileReference UserId))) (SqlPersistT m) ()
go WorkflowActionInfo{ waiIx = stIx, waiFrom = wpFrom, waiHistory = history@(workflowStateCurrentPayloads -> currentPayload), waiAction = WorkflowAction{..} } = maybeT (return ()) $ do go WorkflowActionInfo{ waiIx = stIx, waiFrom = wpFrom, waiHistory = history@(workflowStateCurrentPayloads -> currentPayload), waiAction = WorkflowAction{..} } = maybeT_ $ do
mAuthId <- maybeAuthId mAuthId <- maybeAuthId
stCID <- encryptWorkflowStateIndex wwId stIx stCID <- encryptWorkflowStateIndex wwId stIx

View File

@ -617,7 +617,7 @@ handleJobs' wNum = C.mapM_ $ \jctl -> hoist delimitInternalState . withJobWorker
E.where_ $ matchesPrewarmSource eRef jcPrewarmSource E.where_ $ matchesPrewarmSource eRef jcPrewarmSource
return cRef return cRef
sinkChunkCache lh (cRef, (c, range)) = insertLRUHandle lh (cRef, range) jcTargetTime (c, ByteString.length c) sinkChunkCache lh (cRef, (c, range)) = insertLRUHandle lh (cRef, range) jcTargetTime (c, ByteString.length c)
handleCmd JobCtlInhibitInject{..} = maybeT (return ()) $ do handleCmd JobCtlInhibitInject{..} = maybeT_ $ do
PrewarmCacheConf{..} <- MaybeT . getsYesod $ view _appFileSourcePrewarmConf PrewarmCacheConf{..} <- MaybeT . getsYesod $ view _appFileSourcePrewarmConf
let inhibitInterval = IntervalMap.ClosedInterval let inhibitInterval = IntervalMap.ClosedInterval
(addUTCTime (-precStart) jcTargetTime) (addUTCTime (-precStart) jcTargetTime)

View File

@ -20,6 +20,7 @@ import Handler.Utils.DateTime
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import Jobs.Handler.Intervals.Utils import Jobs.Handler.Intervals.Utils
@ -62,7 +63,7 @@ determineCrontab = execWriterT $ do
let let
tellPrewarmJobs :: JobCtlPrewarmSource -> UTCTime -> WriterT (Crontab JobCtl) (ReaderT SqlReadBackend (HandlerFor UniWorX)) () tellPrewarmJobs :: JobCtlPrewarmSource -> UTCTime -> WriterT (Crontab JobCtl) (ReaderT SqlReadBackend (HandlerFor UniWorX)) ()
tellPrewarmJobs jcPrewarmSource jcTargetTime = maybeT (return ()) $ do tellPrewarmJobs jcPrewarmSource jcTargetTime = maybeT_ $ do
PrewarmCacheConf{..} <- hoistMaybe appFileSourcePrewarmConf PrewarmCacheConf{..} <- hoistMaybe appFileSourcePrewarmConf
let let
@ -89,7 +90,7 @@ determineCrontab = execWriterT $ do
| ts' <- iterate (addUTCTime step') $ addUTCTime (subtract precStart . realToFrac $ toRational (precStart - precEnd) * (1 - recip precMaxSpeedup)) jcTargetTime | ts' <- iterate (addUTCTime step') $ addUTCTime (subtract precStart . realToFrac $ toRational (precStart - precEnd) * (1 - recip precMaxSpeedup)) jcTargetTime
] ]
lift . maybeT (return ()) $ do lift . maybeT_ $ do
injectInterval <- fmap abs . MaybeT . getsYesod $ view _appInjectFiles injectInterval <- fmap abs . MaybeT . getsYesod $ view _appInjectFiles
tell $ HashMap.singleton tell $ HashMap.singleton
JobCtlInhibitInject{..} JobCtlInhibitInject{..}
@ -117,7 +118,7 @@ determineCrontab = execWriterT $ do
for_ (max <$> sheetVisibleFrom <*> sheetHintFrom) $ \hFrom -> do for_ (max <$> sheetVisibleFrom <*> sheetHintFrom) $ \hFrom -> do
tellPrewarmJobs (JobCtlPrewarmSheetFile nSheet SheetHint) hFrom tellPrewarmJobs (JobCtlPrewarmSheetFile nSheet SheetHint) hFrom
when (isn't _JobsOffload appJobMode) . maybeT (return ()) $ do when (isn't _JobsOffload appJobMode) . maybeT_ $ do
guard $ maybe True (\aFrom -> abs (diffUTCTime aFrom hFrom) > 300) sheetActiveFrom guard $ maybe True (\aFrom -> abs (diffUTCTime aFrom hFrom) > 300) sheetActiveFrom
guardM $ or2M (return $ maybe True (\sFrom -> abs (diffUTCTime sFrom hFrom) > 300) sheetSolutionFrom) guardM $ or2M (return $ maybe True (\sFrom -> abs (diffUTCTime sFrom hFrom) > 300) sheetSolutionFrom)
(fmap not . lift . lift $ exists [SheetFileType ==. SheetSolution, SheetFileSheet ==. nSheet]) (fmap not . lift . lift $ exists [SheetFileType ==. SheetSolution, SheetFileSheet ==. nSheet])
@ -133,7 +134,7 @@ determineCrontab = execWriterT $ do
for_ (max <$> sheetVisibleFrom <*> sheetSolutionFrom) $ \sFrom -> do for_ (max <$> sheetVisibleFrom <*> sheetSolutionFrom) $ \sFrom -> do
tellPrewarmJobs (JobCtlPrewarmSheetFile nSheet SheetSolution) sFrom tellPrewarmJobs (JobCtlPrewarmSheetFile nSheet SheetSolution) sFrom
when (isn't _JobsOffload appJobMode) . maybeT (return ()) $ do when (isn't _JobsOffload appJobMode) . maybeT_ $ do
guard $ maybe True (\aFrom -> abs (diffUTCTime aFrom sFrom) > 300) sheetActiveFrom guard $ maybe True (\aFrom -> abs (diffUTCTime aFrom sFrom) > 300) sheetActiveFrom
guardM . lift . lift $ exists [SheetFileType ==. SheetSolution, SheetFileSheet ==. nSheet] guardM . lift . lift $ exists [SheetFileType ==. SheetSolution, SheetFileSheet ==. nSheet]
tell $ HashMap.singleton tell $ HashMap.singleton
@ -386,6 +387,28 @@ determineCrontab = execWriterT $ do
) )
.| C.fold collateSubmissionsByCorrector Map.empty .| C.fold collateSubmissionsByCorrector Map.empty
submissionRatedNotificationsSince <- lift $ getMigrationTime Migration20210318CrontabSubmissionRatedNotification
whenIsJust submissionRatedNotificationsSince $ \notifySince
-> let submissionsSelect = E.selectSource . E.from $ \(submission `E.InnerJoin` sheet) -> do
E.on $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId
E.where_ $ sqlSubmissionRatingDone submission
E.&&. submission E.^. SubmissionRatingTime E.>=. E.justVal notifySince
return (submission, sheet E.^. SheetType)
submissionJobs (Entity subId Submission{..}, E.Value sType) = maybeT_ $ do
examFinishedTime <- hoist lift . for (sType ^? _examPart . from _SqlKey) $ \epId -> do
ExamPart{examPartExam} <- MaybeT . $cachedHereBinary epId $ get epId
Exam{..} <- MaybeT . $cachedHereBinary examPartExam $ get examPartExam
return examFinished
notifyTime <- hoistMaybe . nBot $ maybe NTop (max `on` NTop) examFinishedTime submissionRatingTime
tell $ HashMap.singleton
(JobCtlQueue . JobQueueNotification $ NotificationSubmissionRated subId)
Cron
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ notifyTime
, cronRepeat = CronRepeatNever
, cronRateLimit = appNotificationRateLimit
, cronNotAfter = Left appNotificationExpiration
}
in runConduit $ transPipe lift submissionsSelect .| C.mapM_ submissionJobs
let let
examSelect = E.selectSource . E.from $ \(exam `E.InnerJoin` course `E.InnerJoin` school) -> do examSelect = E.selectSource . E.from $ \(exam `E.InnerJoin` course `E.InnerJoin` school) -> do

View File

@ -268,7 +268,7 @@ dispatchJobPruneUnreferencedFiles numIterations epoch iteration = JobHandlerAtom
dispatchJobInjectFiles :: JobHandler UniWorX dispatchJobInjectFiles :: JobHandler UniWorX
dispatchJobInjectFiles = JobHandlerException . maybeT (return ()) $ do dispatchJobInjectFiles = JobHandlerException . maybeT_ $ do
uploadBucket <- getsYesod $ view _appUploadCacheBucket uploadBucket <- getsYesod $ view _appUploadCacheBucket
interval <- getsYesod $ view _appInjectFiles interval <- getsYesod $ view _appInjectFiles
@ -338,7 +338,7 @@ dispatchJobInjectFiles = JobHandlerException . maybeT (return ()) $ do
Just <$> waitAsync dbAsync Just <$> waitAsync dbAsync
let matchesFRef = is _Just $ assertM (== fRef) fRef' let matchesFRef = is _Just $ assertM (== fRef) fRef'
if | matchesFRef -> if | matchesFRef ->
maybeT (return ()) . runAppMinio . handleIf minioIsDoesNotExist (const $ return ()) $ Minio.removeObject uploadBucket obj maybeT_ . runAppMinio . handleIf minioIsDoesNotExist (const $ return ()) $ Minio.removeObject uploadBucket obj
| otherwise -> | otherwise ->
$logErrorS "InjectFiles" [st|Minio object #{obj}'s content does not match it's name (content hash: #{tshow fRef'} /= name hash: #{tshow fRef})|] $logErrorS "InjectFiles" [st|Minio object #{obj}'s content does not match it's name (content hash: #{tshow fRef'} /= name hash: #{tshow fRef})|]
return . bool mempty (Sum 1, Sum sz) $ is _Just fRef' return . bool mempty (Sum 1, Sum sz) $ is _Just fRef'

View File

@ -14,29 +14,34 @@ import qualified Data.CaseInsensitive as CI
dispatchNotificationSubmissionRated :: SubmissionId -> UserId -> Handler () dispatchNotificationSubmissionRated :: SubmissionId -> UserId -> Handler ()
dispatchNotificationSubmissionRated nSubmission jRecipient = userMailT jRecipient $ do dispatchNotificationSubmissionRated nSubmission jRecipient = maybeT_ $ do
(Course{..}, Sheet{..}, Submission{..}, corrector, sheetTypeDesc) <- liftHandler . runDB $ do (Course{..}, Sheet{..}, Submission{..}, corrector, sheetTypeDesc, hasAccess, csid) <- lift . runDB $ do
submission@Submission{submissionRatingBy} <- getJust nSubmission submission@Submission{submissionRatingBy} <- getJust nSubmission
sheet <- belongsToJust submissionSheet submission sheet@Sheet{sheetName} <- belongsToJust submissionSheet submission
course <- belongsToJust sheetCourse sheet course@Course{..} <- belongsToJust sheetCourse sheet
corrector <- traverse getJust submissionRatingBy corrector <- traverse getJust submissionRatingBy
sheetTypeDesc <- sheetTypeDescription (sheetCourse sheet) (sheetType sheet) sheetTypeDesc <- sheetTypeDescription (sheetCourse sheet) (sheetType sheet)
return (course, sheet, submission, corrector, sheetTypeDesc) csid <- encrypt nSubmission
whenIsJust corrector $ \corrector' -> hasAccess <- is _Authorized <$> evalAccessForDB (Just jRecipient) (CSubmissionR courseTerm courseSchool courseShorthand sheetName csid CorrectionR) False
addMailHeader "Reply-To" . renderAddress $ userAddressFrom corrector' return (course, sheet, submission, corrector, sheetTypeDesc, hasAccess, csid)
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
csid <- encrypt nSubmission guard hasAccess
MsgRenderer mr <- getMailMsgRenderer
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
submissionRatingTime' <- traverse (formatTimeMail SelFormatDateTime) submissionRatingTime
let tid = courseTerm
ssh = courseSchool
csh = courseShorthand
shn = sheetName
editNotifications <- mkEditNotifications jRecipient lift . userMailT jRecipient $ do
whenIsJust corrector $ \corrector' ->
addMailHeader "Reply-To" . renderAddress $ userAddressFrom corrector'
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
addHtmlMarkdownAlternatives ($(ihamletFile "templates/mail/submissionRated.hamlet") :: HtmlUrlI18n (SomeMessage UniWorX) (Route UniWorX)) MsgRenderer mr <- getMailMsgRenderer
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
submissionRatingTime' <- traverse (formatTimeMail SelFormatDateTime) submissionRatingTime
let tid = courseTerm
ssh = courseSchool
csh = courseShorthand
shn = sheetName
editNotifications <- mkEditNotifications jRecipient
addHtmlMarkdownAlternatives ($(ihamletFile "templates/mail/submissionRated.hamlet") :: HtmlUrlI18n (SomeMessage UniWorX) (Route UniWorX))

View File

@ -3,6 +3,7 @@
module Model.Migration module Model.Migration
( migrateAll ( migrateAll
, requiresMigration , requiresMigration
, ManualMigration(..), getMigrationTime
) where ) where
import Import.NoModel hiding (Max(..), Last(..)) import Import.NoModel hiding (Max(..), Last(..))
@ -184,3 +185,11 @@ getMissingMigrations = do
E.where_ $ appliedMigration E.^. AppliedMigrationMigration `E.in_` E.valList universeF E.where_ $ appliedMigration E.^. AppliedMigrationMigration `E.in_` E.valList universeF
return $ appliedMigration E.^. AppliedMigrationMigration return $ appliedMigration E.^. AppliedMigrationMigration
return $ customMigrations `Map.withoutKeys` Set.fromList appliedMigrations return $ customMigrations `Map.withoutKeys` Set.fromList appliedMigrations
getMigrationTime :: ( MonadIO m
, BaseBackend backend ~ SqlBackend
, PersistStoreRead backend
)
=> ManualMigration
-> ReaderT backend m (Maybe UTCTime)
getMigrationTime = fmap (fmap appliedMigrationTime) . get . AppliedMigrationKey

View File

@ -101,6 +101,7 @@ data ManualMigration
| Migration20210115ExamPartsFrom | Migration20210115ExamPartsFrom
| Migration20210201SharedWorkflowGraphs | Migration20210201SharedWorkflowGraphs
| Migration20210208StudyFeaturesRelevanceCachedUUIDs | Migration20210208StudyFeaturesRelevanceCachedUUIDs
| Migration20210318CrontabSubmissionRatedNotification
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite) deriving anyclass (Universe, Finite)
@ -142,6 +143,7 @@ migrateManual = do
, ("study_features_relevance_cached", "CREATE INDEX study_features_relevance_cached ON \"study_features\" (relevance_cached)") , ("study_features_relevance_cached", "CREATE INDEX study_features_relevance_cached ON \"study_features\" (relevance_cached)")
, ("submission_rating_by", "CREATE INDEX submission_rating_by ON submission (rating_by) WHERE rating_by IS NOT NULL" ) , ("submission_rating_by", "CREATE INDEX submission_rating_by ON submission (rating_by) WHERE rating_by IS NOT NULL" )
, ("exam_corrector_user", "CREATE INDEX exam_corrector_user ON exam_corrector (\"user\")" ) , ("exam_corrector_user", "CREATE INDEX exam_corrector_user ON exam_corrector (\"user\")" )
, ("submission_rating_time", "CREATE INDEX submission_rating_time ON submission (rating_time)" )
] ]
where where
addIndex :: Text -> Sql -> Migration addIndex :: Text -> Sql -> Migration
@ -1042,6 +1044,9 @@ customMigrations = mapF $ \case
ALTER TABLE "study_features" RENAME COLUMN "relevance_cached_uuid" TO "relevance_cached"; ALTER TABLE "study_features" RENAME COLUMN "relevance_cached_uuid" TO "relevance_cached";
|] |]
-- Placeholder to inform crontab generation when switchover happened so old submissions don't get notified as corrected
Migration20210318CrontabSubmissionRatedNotification -> return ()
tableExists :: MonadIO m => Text -> ReaderT SqlBackend m Bool tableExists :: MonadIO m => Text -> ReaderT SqlBackend m Bool
tableExists table = do tableExists table = do

View File

@ -993,7 +993,7 @@ maxLength l = not . minLength (succ l)
tellM :: (MonadTrans t, MonadWriter x (t m), Monad m) => m x -> t m () tellM :: (MonadTrans t, MonadWriter x (t m), Monad m) => m x -> t m ()
tellM = tell <=< lift tellM = tell <=< lift
tellPoint :: (MonadWriter mono m, MonoPointed mono) => Element mono -> m () tellPoint :: forall mono m. (MonadWriter mono m, MonoPointed mono) => Element mono -> m ()
tellPoint = tell . opoint tellPoint = tell . opoint
tellMPoint :: (MonadTrans t, MonadWriter mono (t m), Monad m, MonoPointed mono) => m (Element mono) -> t m () tellMPoint :: (MonadTrans t, MonadWriter mono (t m), Monad m, MonoPointed mono) => m (Element mono) -> t m ()

View File

@ -216,7 +216,7 @@ replaceFileReferences' mkFilter residual = do
= modify $ Map.mapMaybe (assertM' (not . Set.null) . (\\ sfIds)) = modify $ Map.mapMaybe (assertM' (not . Set.null) . (\\ sfIds))
| otherwise = do | otherwise = do
let fRef' = _FileReference # (fRef, residual) let fRef' = _FileReference # (fRef, residual)
forM_ (persistUniqueKeys fRef') $ \u -> maybeT (return ()) $ do forM_ (persistUniqueKeys fRef') $ \u -> maybeT_ $ do
Entity cKey cVal <- MaybeT . lift $ getBy u Entity cKey cVal <- MaybeT . lift $ getBy u
deleted <- lift . lift . deleteWhereCount $ resFilter <> [ persistIdField ==. cKey ] deleted <- lift . lift . deleteWhereCount $ resFilter <> [ persistIdField ==. cKey ]
unless (deleted == 1) $ unless (deleted == 1) $

View File

@ -2,9 +2,9 @@ $newline never
_{MsgCorrectionInvisibleWarning} _{MsgCorrectionInvisibleWarning}
$if not (null correctionInvisibleReasons) $if not (null reasons)
<br /> <br />
_{MsgCorrectionInvisibleReasons} _{MsgCorrectionInvisibleReasons}
<ul> <ul>
$forall reason <- correctionInvisibleReasons $forall reason <- reasons
<li>_{reason} <li>_{reason}