From 782110a824405143f35ef13ab56e0c45d14b3435 Mon Sep 17 00:00:00 2001 From: Steffen Jost Date: Wed, 19 Jun 2019 11:11:55 +0200 Subject: [PATCH 1/4] feat minor: E.isJust added --- src/Database/Esqueleto/Utils.hs | 7 ++++++- src/Handler/Corrections.hs | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Database/Esqueleto/Utils.hs b/src/Database/Esqueleto/Utils.hs index 38105a37a..bd8120ba7 100644 --- a/src/Database/Esqueleto/Utils.hs +++ b/src/Database/Esqueleto/Utils.hs @@ -2,6 +2,7 @@ module Database.Esqueleto.Utils ( true, false + , isJust , isInfixOf, hasInfix , any, all , SqlIn(..) @@ -11,7 +12,7 @@ module Database.Esqueleto.Utils , anyFilter, allFilter ) where -import ClassyPrelude.Yesod hiding (isInfixOf, any, all) +import ClassyPrelude.Yesod hiding (isInfixOf, any, all, isJust) import qualified Data.Set as Set import qualified Data.Foldable as F import qualified Database.Esqueleto as E @@ -34,6 +35,10 @@ true = E.val True false :: E.SqlExpr (E.Value Bool) false = E.val False +-- | Negation of `isNothing` which is missing +isJust :: (E.Esqueleto query expr backend, PersistField typ) => expr (E.Value (Maybe typ)) -> expr (E.Value Bool) +isJust = E.not_ . E.isNothing + -- | Check if the first string is contained in the text derived from the second argument isInfixOf :: (E.Esqueleto query expr backend, E.SqlString s2) => Text -> expr (E.Value s2) -> expr (E.Value Bool) diff --git a/src/Handler/Corrections.hs b/src/Handler/Corrections.hs index dc8cb791e..92a60d00e 100644 --- a/src/Handler/Corrections.hs +++ b/src/Handler/Corrections.hs @@ -337,13 +337,13 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d , ( "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.not_ . E.isNothing $ submission E.^. SubmissionRatingBy + 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.not_ . E.isNothing $ submission E.^. SubmissionRatingTime + 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 From 16d422d9d82467ea6e6800bee5d1af06b7fe1d3b Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Tue, 25 Jun 2019 20:45:46 +0200 Subject: [PATCH 2/4] fix(fe-deflist): avoid horizontal scroll on pages with deflist --- templates/default-layout.lucius | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/default-layout.lucius b/templates/default-layout.lucius index 9db97efb6..6306920e2 100644 --- a/templates/default-layout.lucius +++ b/templates/default-layout.lucius @@ -502,7 +502,7 @@ ul.list--inline { @media (min-width: 768px) { .deflist { - grid-template-columns: max-content minmax(auto, max-content); + grid-template-columns: max-content minmax(0, max-content); .deflist { margin-top: -10px; From c82c3a9d80c4111b77ecda6ead30fdae25106b37 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Tue, 25 Jun 2019 23:09:47 +0200 Subject: [PATCH 3/4] chore: small commit to get the pipeline started --- templates/default-layout.lucius | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/default-layout.lucius b/templates/default-layout.lucius index 6306920e2..2fdc1b3de 100644 --- a/templates/default-layout.lucius +++ b/templates/default-layout.lucius @@ -580,7 +580,7 @@ section { justify-content: center; } } - + .form-group__input > .notification { margin: 0; } From 2f7f733628a1c57e5eb2690bfd218b4d8cb023fd Mon Sep 17 00:00:00 2001 From: Steffen Jost Date: Wed, 26 Jun 2019 12:12:19 +0200 Subject: [PATCH 4/4] =?UTF-8?q?Abgaben=20Zuteilung=20=C3=BCberarbeitet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/tasks.json | 1 + messages/uniworx/de.msg | 15 +++++- src/Foundation.hs | 9 ++++ src/Handler/Corrections.hs | 69 ++++++++++++++++++--------- src/Handler/Utils/Submission.hs | 14 ++++-- templates/corrections-overview.hamlet | 63 +++++++++++++++++------- 6 files changed, 125 insertions(+), 46 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fabc6a5d5..4c18542ba 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -14,6 +14,7 @@ "reveal": "always", "focus": false, "panel": "dedicated", + "clear": true, "showReuseMessage": false } }, diff --git a/messages/uniworx/de.msg b/messages/uniworx/de.msg index c0ee216ae..455b57773 100644 --- a/messages/uniworx/de.msg +++ b/messages/uniworx/de.msg @@ -34,6 +34,12 @@ GenericShort: Kürzel GenericIsNew: Neu GenericHasConflict: Konflikt GenericBack: Zurück +GenericChange: Änderung +GenericNumChange: +/- +GenericMin: Min +GenericAvg: Avg +GenericMax: Max +GenericAll: Insgesamt SummerTerm year@Integer: Sommersemester #{display year} WinterTerm year@Integer: Wintersemester #{display year}/#{display $ succ year} @@ -316,6 +322,7 @@ Correctors: Korrektoren CorState: Status CorByTut: Zuteilung nach Tutorium CorProportion: Anteil +CorDeficit: Defizit CorByProportionOnly proportion@Rational: #{display proportion} Anteile CorByProportionIncludingTutorial proportion@Rational: #{display proportion} Anteile - Tutorium CorByProportionExcludingTutorial proportion@Rational: #{display proportion} Anteile + Tutorium @@ -391,10 +398,11 @@ UpdatedAssignedCorrectorSingle num@Int64: #{display num} Abgaben wurden dem neue NoCorrector: Kein Korrektor RemovedCorrections num@Int64: Korrektur-Daten wurden von #{display num} Abgaben entfernt. UpdatedAssignedCorrectorsAuto num@Int64: #{display num} Abgaben wurden unter den Korrektoren aufgeteilt. +UpdatedSheetCorrectorsAutoAssigned n@Int: #{display n} #{pluralDE n "Abgabe wurde einem Korrektor" "Abgaben wurden Korrektoren"} zugteilt. +UpdatedSheetCorrectorsAutoFailed n@Int: #{display n} #{pluralDE n "Abgabe konnte" "Abgaben konnten"} nicht automatisch zugewiesen werden. CouldNotAssignCorrectorsAuto num@Int64: #{display num} Abgaben konnten nicht automatisch zugewiesen werden: SelfCorrectors num@Int64: #{display num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt! - CorrectionSheets: Übersicht Korrekturen nach Blättern CorrectionCorrectors: Übersicht Korrekturen nach Korrektoren AssignSubmissionExceptionNoCorrectors: Es sind keine Korrektoren eingestellt @@ -402,13 +410,15 @@ AssignSubmissionExceptionNoCorrectorsByProportion: Es sind keine Korrektoren mit AssignSubmissionExceptionSubmissionsNotFound n@Int: #{tshow n} Abgaben konnten nicht gefunden werden NrSubmittorsTotal: Abgebende NrSubmissionsTotal: Abgaben +NrSubmissionsTotalShort: Abg. NrSubmissionsUnassigned: Ohne Korrektor NoCorrectorAssigned: Ohne Korrektor NrCorrectors: Korrektoren NrSubmissionsNewlyAssigned: Neu zugeteilt NrSubmissionsNotAssigned: Nicht zugeteilt NrSubmissionsNotCorrected: Unkorrigiert -CorrectionTime: Korrekturdauer (Min/Avg/Max) +NrSubmissionsNotCorrectedShort: Unkg. +CorrectionTime: Korrekturdauer AssignSubmissionsRandomWarning: Die Zuteilungsvorschau kann von der tatsächlichen Zuteilung abweichen, wenn mehrere Blätter auf einmal zugeteilt werden, da beim Ausgleich der Kontigente nur bereits zugeteilte Abgaben berücksichtigt werden. Da es ein randomisierte Prozess ist, kann es auch bei einzelnen Blättern gerinfgügige Abweichungen geben. CorrectionsUploaded num@Int64: #{display num} Korrekturen wurden gespeichert: @@ -831,6 +841,7 @@ MenuCourseDelete: Kurs löschen MenuSubmissionNew: Abgabe anlegen MenuSubmissionOwn: Abgabe MenuCorrectors: Korrektoren +MenuCorrectorsChange: Korrektoren ändern MenuSheetEdit: Übungsblatt editieren MenuSheetDelete: Übungsblatt löschen MenuSheetClone: Als neues Übungsblatt klonen diff --git a/src/Foundation.hs b/src/Foundation.hs index 3be718bb7..3e243af41 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -206,6 +206,15 @@ noneOneMoreDE num noneText singularForm pluralForm | num == 1 = singularForm | otherwise = pluralForm +noneMoreDE :: (Eq a, Num a) + => a -- ^ Count + -> Text -- ^ None + -> Text -- ^ Some + -> Text +noneMoreDE num noneText someText + | num == 0 = noneText + | otherwise = someText + -- Convenience Type for Messages, since Yesod messages cannot deal with compound type identifiers type IntMaybe = Maybe Int type TextList = [Text] diff --git a/src/Handler/Corrections.hs b/src/Handler/Corrections.hs index 92a60d00e..bc46eb157 100644 --- a/src/Handler/Corrections.hs +++ b/src/Handler/Corrections.hs @@ -1137,30 +1137,33 @@ assignHandler tid ssh csh cid assignSids = do in List.foldr foldFun False sheetList -- plan or assign unassigned submissions for given sheets - let buildA :: (Map SheetName ((Set SubmissionId, Set SubmissionId), Map (Maybe UserId) Int)) -> SheetId -> DB (Map SheetName ((Set SubmissionId, Set SubmissionId), Map (Maybe UserId) Int)) + let buildA :: (Map SheetName ((Set SubmissionId, Set SubmissionId), Map (Maybe UserId) Int, Map UserId Rational)) -> SheetId -> DB (Map SheetName ((Set SubmissionId, Set SubmissionId), Map (Maybe UserId) Int, Map UserId Rational)) buildA acc sid = maybeT (return acc) $ do let shn = sheetName $ sheets ! sid -- is sheet closed? guardM $ lift $ hasWriteAccessTo $ CSheetR tid ssh csh shn SAssignR -- we must check, whether the submission is already closed and thus assignable - -- has at least one uncorrected / unassigned submisison? - [E.Value hasSubmission] <- lift $ E.select $ return $ E.exists $ E.from $ \submission -> do - E.where_ $ submission E.^. SubmissionSheet E.==. E.val sid - E.where_ $ E.isNothing $ submission E.^. SubmissionRatingBy -- no corrector - E.where_ $ E.isNothing $ submission E.^. SubmissionRatingTime -- not done - guard hasSubmission - -- has at least one active corrector? - [E.Value hasCorrector] <- lift $ E.select $ return $ E.exists $ E.from $ \corrector -> do - E.where_ $ corrector E.^. SheetCorrectorSheet E.==. E.val sid - E.where_ $ corrector E.^. SheetCorrectorState E.==. E.val CorrectorNormal - -- E.where_ $ corrector E.^. SheetCorrectorLoad E./=. E.val (Load {byTutorial = Nothing, byProportion = 0}) - guard hasCorrector - -- TODO: Refactor guards above! We already have these informations, but forcing the maps inside the DB acces might not be a good idea - -- TODO: Maybe refactor planSubmissions instead to not throw exceptions, but signal "ok" or "not possible" instead! - plan <- lift $ planSubmissions sid Nothing + -- ask for assignment plan + let ignoreExceptions :: AssignSubmissionException -> DB (Map SubmissionId (Maybe UserId), Map UserId Rational) -- silently ignore errors, since we check all sheets and only care about sheets with unassigned corrections + ignoreExceptions NoCorrectors = return mempty + ignoreExceptions NoCorrectorsByProportion = return mempty + ignoreExceptions (SubmissionsNotFound _sids_not_found) = return mempty -- cannot happen, since last argument to planSubmissions is Nothing + (plan,deficit) <- lift $ handle ignoreExceptions $ planSubmissions sid Nothing + guard $ not $ null plan -- only proceed if there is a plan for this sheet + -- implement assignment plan status <- lift $ case btnResult of Nothing -> return (Set.empty, Set.empty) - (Just BtnSubmissionsAssign) -> writeSubmissionPlan plan -- TODO: this comes to late!! - return $ Map.insert shn (status, countMapElems plan) acc + (Just BtnSubmissionsAssign) -> do + status@(sub_ok,sub_fail) <- writeSubmissionPlan plan + let nr_ok = olength sub_ok + nr_fail = olength sub_fail + alert_ok = toMaybe (nr_ok > 0) $ SomeMessage $ MsgUpdatedSheetCorrectorsAutoAssigned nr_ok + alert_fail = toMaybe (nr_fail > 0) $ SomeMessage $ MsgUpdatedSheetCorrectorsAutoFailed nr_fail + msg_status = bool Success Error $ nr_fail > 0 + msg_header = SomeMessage $ shn <> ":" + when (nr_ok > 0 || nr_fail > 0) $ + addMessageI msg_status $ UniWorXMessages $ msg_header : catMaybes [alert_ok, alert_fail] + return status + return $ Map.insert shn (status, countMapElems plan, deficit) acc assignment <- foldM buildA Map.empty assignSids correctors <- E.select . E.from $ \(corrector `E.InnerJoin` user) -> do @@ -1211,28 +1214,50 @@ assignHandler tid ssh csh cid assignSids = do -- create aggregate maps sheetMap :: Map SheetName CorrectionInfo sheetMap = Map.map fold infoMap + + sheetLoad :: Map SheetName Load + sheetLoad = -- Map.unionsWith (<>) ((Map.filter () ) . snd) <$> Map.elems correctorMap) + let buildSL acc (_user,mSnSc) = Map.foldlWithKey buildL acc mSnSc + buildL acc s SheetCorrector{sheetCorrectorLoad=l, sheetCorrectorState=CorrectorNormal} + = Map.insertWith (<>) s l acc + buildL acc _ _ = acc + in Map.foldl buildSL Map.empty correctorMap + + deficitMap :: Map UserId Rational + deficitMap = foldMap (view _3) assignment + corrMap :: Map (Maybe UserId) CorrectionInfo corrMap = Map.unionsWith (<>) $ Map.elems infoMap sheetNames = Map.keys infoMap let -- whamlet convenience functions - -- avoid nestes hamelt $maybe with duplicated $nothing + -- avoid nestes hamlet $maybe with duplicated $nothing getCorrector :: Maybe UserId -> (Widget,Map SheetName SheetCorrector) getCorrector (Just uid) | Just (User{..},loadMap) <- Map.lookup uid correctorMap = (nameEmailWidget userEmail userDisplayName userSurname, loadMap) getCorrector _ = ([whamlet|_{MsgNoCorrectorAssigned}|], mempty) - -- avoid nestes hamelt $maybe with duplicated $nothing + -- avoid nestes hamlet $maybe with duplicated $nothing getCorrSheetStatus :: Maybe UserId -> SheetName -> Maybe CorrectionInfo getCorrSheetStatus corr shn | (Just smap) <- Map.lookup shn infoMap = Map.lookup corr smap getCorrSheetStatus _ _ = Nothing - -- avoid nestes hamelt $maybe with duplicated $nothing + -- avoid nestes hamlet $maybe with duplicated $nothing getCorrNewAssignment :: Maybe UserId -> SheetName -> Maybe Int getCorrNewAssignment corr shn - | (Just (_,cass)) <- Map.lookup shn assignment + | (Just (_,cass,_)) <- Map.lookup shn assignment = Map.lookup corr cass getCorrNewAssignment _ _ = Nothing + -- avoid nestes hamlet $maybe with duplicated $nothing + getCorrDeficit :: Maybe UserId -> Maybe Rational + getCorrDeficit (Just uid) = Map.lookup uid deficitMap + getCorrDeficit _ = Nothing + + getLoadSum :: SheetName -> Text + getLoadSum shn + | (Just load) <- Map.lookup shn sheetLoad + = "Σ" <> showCompactCorrectorLoad load CorrectorNormal + getLoadSum _ = mempty showDiffDays :: Maybe NominalDiffTime -> Text showDiffDays = foldMap formatDiffDays diff --git a/src/Handler/Utils/Submission.hs b/src/Handler/Utils/Submission.hs index 7cb28cfa5..0a1e04e9c 100644 --- a/src/Handler/Utils/Submission.hs +++ b/src/Handler/Utils/Submission.hs @@ -66,7 +66,9 @@ assignSubmissions :: SheetId -- ^ Sheet to distribute to correctors -> YesodDB UniWorX ( Set SubmissionId , Set SubmissionId ) -- ^ Returns assigned and unassigned submissions; unassigned submissions occur only if no tutors have an assigned load -assignSubmissions sid restriction = planSubmissions sid restriction >>= writeSubmissionPlan +assignSubmissions sid restriction = do + (plan,_) <- planSubmissions sid restriction + writeSubmissionPlan plan -- | Assigns all submissions according to an already given assignment plan writeSubmissionPlan :: Map SubmissionId (Maybe UserId) @@ -89,8 +91,8 @@ writeSubmissionPlan newSubmissionData = do -- May throw an exception if there are no suitable correctors planSubmissions :: SheetId -- ^ Sheet to distribute to correctors -> Maybe (Set SubmissionId) -- ^ Optionally restrict submission to consider - -> YesodDB UniWorX (Map SubmissionId (Maybe UserId)) - -- ^ Return map that assigns submissions to Corrector + -> YesodDB UniWorX (Map SubmissionId (Maybe UserId), Map UserId Rational) + -- ^ Return map that assigns submissions to Corrector and another map showing each current correctors _previous_ deficit planSubmissions sid restriction = do Sheet{..} <- getJust sid correctorsRaw <- E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> do @@ -171,6 +173,10 @@ planSubmissions sid restriction = do -> m b withSubmissionData f = f <$> (mappend <$> ask <*> State.get) + -- | Old Deficit for protocol purposes, not used here + oldDeficit :: Map UserId Rational + oldDeficit = Map.mapWithKey (\k _v -> calculateDeficit k submissionData) sheetCorrectors + -- | How many additional submission should the given corrector be assigned, if possible? calculateDeficit :: UserId -> Map SubmissionId (Maybe UserId, Map UserId _, SheetId) -> Rational calculateDeficit corrector submissionState = getSum $ foldMap Sum deficitBySheet @@ -235,7 +241,7 @@ planSubmissions sid restriction = do ix subId . _1 <~ Just <$> liftIO (Rand.uniform bestCorrectors) - return $ fmap (view _1) newSubmissionData + return (fmap (view _1) newSubmissionData, oldDeficit) where maximumsBy :: (Ord a, Ord b) => (a -> b) -> Set a -> Set a maximumsBy f xs = flip Set.filter xs $ \x -> maybe True (((==) `on` f) x . maximumBy (comparing f)) $ fromNullable xs diff --git a/templates/corrections-overview.hamlet b/templates/corrections-overview.hamlet index 621ffd51e..5d3f6ba8b 100644 --- a/templates/corrections-overview.hamlet +++ b/templates/corrections-overview.hamlet @@ -3,20 +3,26 @@ _{MsgCourseParticipants nrParticipants} - +
_{MsgSheet} + _{MsgSheet} $if groupsPossible - _{MsgNrSubmittorsTotal} - _{MsgNrSubmissionsTotal} + _{MsgNrSubmittorsTotal} + _{MsgNrSubmissionsTotal} _{MsgNrSubmissionsNotAssigned} - _{MsgNrSubmissionsNotCorrected} + _{MsgNrSubmissionsNotCorrected} _{MsgCorrectionTime} +
+ _{MsgGenericNumChange} + _{MsgGenericMin} + _{MsgGenericAvg} + _{MsgGenericMax} $forall (sheetName, CorrectionInfo{ciSubmittors, ciSubmissions, ciAssigned, ciCorrected, ciMin, ciTot, ciMax}) <- Map.toList sheetMap
^{simpleLink (toWidget sheetName) (CSheetR tid ssh csh sheetName SSubsR)} $if groupsPossible #{ciSubmittors} #{ciSubmissions} - $maybe ((splus,sfailed),_) <- Map.lookup sheetName assignment + $maybe ((splus,sfailed),_,_) <- Map.lookup sheetName assignment $if 0 < Set.size sfailed #{ciSubmissions - ciAssigned} (-#{show (Set.size splus)}, failed: #{show (Set.size sfailed)}) @@ -24,9 +30,11 @@ #{ciSubmissions - ciAssigned} (-#{show (Set.size splus)}) $else - #{ciSubmissions - ciAssigned} + #{ciSubmissions - ciAssigned} + $nothing - #{ciSubmissions - ciAssigned} + #{ciSubmissions - ciAssigned} + #{ciSubmissions - ciCorrected} #{showDiffDays ciMin} #{showAvgsDays ciTot ciCorrected} @@ -35,43 +43,62 @@

_{MsgCorrectionCorrectors} - +
_{MsgCorrector} - _{MsgNrSubmissionsTotal} - _{MsgNrSubmissionsNotCorrected} + _{MsgCorrector} + _{MsgGenericAll} + _{MsgCorProportion} _{MsgCorrectionTime} $forall shn <- sheetNames #{shn} $# ^{simpleLinkI (SomeMessage MsgMenuCorrectors) (CSheetR tid ssh csh shn SCorrR)} +
_{MsgNrSubmissionsTotal} + _{MsgNrSubmissionsNotCorrected} + _{MsgCorDeficit} + _{MsgGenericMin} + _{MsgGenericAvg} + _{MsgGenericMax} + $forall _shn <- sheetNames + _{MsgCorProportion} + _{MsgNrSubmissionsTotalShort} + _{MsgGenericNumChange} + _{MsgNrSubmissionsNotCorrectedShort} + _{MsgGenericAvg} $forall (CorrectionInfo{ciCorrector, ciSubmissions, ciCorrected, ciMin, ciTot, ciMax}) <- Map.elems corrMap $with (nameW,loadM) <- getCorrector ciCorrector
^{nameW} #{ciSubmissions} #{ciSubmissions - ciCorrected} + + $maybe deficit <- getCorrDeficit ciCorrector + #{display deficit} #{showDiffDays ciMin} #{showAvgsDays ciTot ciCorrected} #{showDiffDays ciMax} $forall shn <- sheetNames - $maybe SheetCorrector{sheetCorrectorLoad, sheetCorrectorState} <- Map.lookup shn loadM - #{showCompactCorrectorLoad sheetCorrectorLoad sheetCorrectorState} - $nothing - + + $maybe SheetCorrector{sheetCorrectorLoad, sheetCorrectorState} <- Map.lookup shn loadM + #{showCompactCorrectorLoad sheetCorrectorLoad sheetCorrectorState} $maybe CorrectionInfo{ciSubmissions,ciCorrected,ciTot} <- getCorrSheetStatus ciCorrector shn + #{ciSubmissions} $maybe nrNew <- getCorrNewAssignment ciCorrector shn - #{ciSubmissions} $# #{ciAssigned} `ciSubmissions` is here always identical to `ciAssigned` and also works for `ciCorrector == Nothing`. ciAssigned only useful in aggregate maps like `sheetMap` (+#{nrNew}) $nothing - #{ciSubmissions} + #{ciSubmissions - ciCorrected} #{showAvgsDays ciTot ciCorrected} $nothing - + + + + $if 0 < length sheetNames
$forall shn <- sheetNames - ^{simpleLinkI (SomeMessage MsgMenuCorrectors) (CSheetR tid ssh csh shn SCorrR)} + #{getLoadSum shn} + ^{simpleLinkI (SomeMessage MsgMenuCorrectorsChange) (CSheetR tid ssh csh shn SCorrR)} ^{btnWdgt}

_{MsgAssignSubmissionsRandomWarning} \ No newline at end of file