feat(allocations): improve accept ui and logging

This commit is contained in:
Gregor Kleen 2020-03-11 14:16:02 +01:00
parent 20ef95c142
commit 3422fd70a7
9 changed files with 111 additions and 46 deletions

View File

@ -872,7 +872,7 @@ th, td
right: 5px right: 5px
top: 5px top: 5px
.occurrence--not-registered, .no-bonus .occurrence--not-registered, .no-bonus, .allocation-course--excluded
text-decoration: line-through text-decoration: line-through
.result .result

View File

@ -28,6 +28,7 @@ Allocation -- attributes with prefix staff- affect lecturers only, but are invis
AllocationMatching AllocationMatching
allocation AllocationId allocation AllocationId
fingerprint AllocationFingerprint fingerprint AllocationFingerprint
time UTCTime
log FileId log FileId
AllocationCourse AllocationCourse

View File

@ -28,3 +28,7 @@ instance Hashable a => Hashable (NonNull a) where
instance (Binary a, MonoFoldable a) => Binary (NonNull a) where instance (Binary a, MonoFoldable a) => Binary (NonNull a) where
get = Binary.get >>= maybe (fail "Expected non-empty structure") return . fromNullable get = Binary.get >>= maybe (fail "Expected non-empty structure") return . fromNullable
put = Binary.put . toNullable put = Binary.put . toNullable
instance NFData a => NFData (NonNull a) where
rnf = rnf . toNullable

View File

@ -17,6 +17,8 @@ import qualified Control.Monad.State.Class as State
import Data.Semigroup (Dual(..)) import Data.Semigroup (Dual(..))
import Data.Sequence (Seq((:|>)))
newtype SessionDataAllocationResults = SessionDataAllocationResults newtype SessionDataAllocationResults = SessionDataAllocationResults
{ getSessionDataAllocationResults :: Map ( TermId { getSessionDataAllocationResults :: Map ( TermId
@ -26,11 +28,11 @@ newtype SessionDataAllocationResults = SessionDataAllocationResults
( UTCTime ( UTCTime
, AllocationFingerprint , AllocationFingerprint
, Set (UserId, CourseId) , Set (UserId, CourseId)
, Seq (MatchingLog UserId CourseId Natural) , Seq MatchingLogRun
) )
} deriving (Eq, Ord, Read, Show, Generic, Typeable) } deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving newtype (ToJSON, FromJSON) deriving newtype (ToJSON, FromJSON)
deriving (Monoid, Semigroup) via Dual (Map (TermId, SchoolId, AllocationShorthand) (UTCTime, AllocationFingerprint, Set (UserId, CourseId), Seq (MatchingLog UserId CourseId Natural))) deriving (Monoid, Semigroup) via Dual (Map (TermId, SchoolId, AllocationShorthand) (UTCTime, AllocationFingerprint, Set (UserId, CourseId), Seq MatchingLogRun))
makeWrapped ''SessionDataAllocationResults makeWrapped ''SessionDataAllocationResults
@ -47,11 +49,11 @@ instance Button UniWorX AllocationAcceptButton where
btnClasses BtnAllocationAccept = [BCIsButton, BCPrimary] btnClasses BtnAllocationAccept = [BCIsButton, BCPrimary]
allocationAcceptForm :: AllocationId -> DB (Maybe (Form (UTCTime, AllocationFingerprint, Set (UserId, CourseId), Seq (MatchingLog UserId CourseId Natural)))) allocationAcceptForm :: AllocationId -> DB (Maybe (Form (UTCTime, AllocationFingerprint, Set (UserId, CourseId), Seq MatchingLogRun)))
allocationAcceptForm aId = runMaybeT $ do allocationAcceptForm aId = runMaybeT $ do
Allocation{..} <- MaybeT $ get aId Allocation{..} <- MaybeT $ get aId
SessionDataAllocationResults allocMap <- MaybeT $ lookupSessionJson SessionAllocationResults SessionDataAllocationResults allocMap <- MaybeT $ lookupSessionJson SessionAllocationResults
allocRes@(allocTime, allocFp, allocMatching, _) <- hoistMaybe $ allocMap !? (allocationTerm, allocationSchool, allocationShorthand) allocRes@(allocTime, allocFp, allocMatching, _ :|> MatchingLogRun{..}) <- hoistMaybe $ allocMap !? (allocationTerm, allocationSchool, allocationShorthand)
$logInfoS "allocationAcceptForm" $ tshow allocRes $logInfoS "allocationAcceptForm" $ tshow allocRes
allocationUsers <- fmap (map $ bimap E.unValue E.unValue) . lift . E.select . E.from $ \allocationUser -> do allocationUsers <- fmap (map $ bimap E.unValue E.unValue) . lift . E.select . E.from $ \allocationUser -> do

View File

@ -107,8 +107,7 @@ getAUsersR = postAUsersR
postAUsersR tid ssh ash = do postAUsersR tid ssh ash = do
(usersTable, acceptForm) <- runDB $ do (usersTable, acceptForm) <- runDB $ do
Entity aId _ <- getBy404 $ TermSchoolAllocationShort tid ssh ash Entity aId _ <- getBy404 $ TermSchoolAllocationShort tid ssh ash
now <- liftIO getCurrentTime resultsDone <- is _Just <$> allocationStarted aId
resultsDone <- (<= NTop (Just now)) . NTop <$> allocationDone aId
csvName <- getMessageRender <*> pure (MsgAllocationUsersCsvName tid ssh ash) csvName <- getMessageRender <*> pure (MsgAllocationUsersCsvName tid ssh ash)

View File

@ -1,7 +1,8 @@
module Handler.Utils.Allocation module Handler.Utils.Allocation
( allocationDone ( allocationStarted
, ordinalPriorities , ordinalPriorities
, sinkAllocationPriorities , sinkAllocationPriorities
, MatchingLogRun(..)
, computeAllocation , computeAllocation
-- , doAllocation -- Use `storeAllocationResult` -- , doAllocation -- Use `storeAllocationResult`
, ppMatchingLog , ppMatchingLog
@ -15,6 +16,7 @@ import qualified Data.Map.Strict as Map
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import Control.Monad.Trans.State (execStateT)
import qualified Control.Monad.State.Class as State (get, modify') import qualified Control.Monad.State.Class as State (get, modify')
import Data.List (genericLength, elemIndex) import Data.List (genericLength, elemIndex)
@ -33,13 +35,36 @@ import Data.Generics.Product.Param
import qualified Crypto.Hash as Crypto import qualified Crypto.Hash as Crypto
import qualified Data.Binary as Binary import qualified Data.Binary as Binary
import qualified Data.ByteArray as BA (convert)
data MatchingExcludedReason
= MatchingExcludedParticipationExisted
| MatchingExcludedParticipationExists
| MatchingExcludedVeto
| MatchingExcludedLecturer
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite, NFData)
nullaryPathPiece ''MatchingExcludedReason $ camelToPathPiece' 2
pathPieceJSON ''MatchingExcludedReason
data MatchingLogRun = MatchingLogRun
{ matchingLogRunCourseRestriction :: Maybe (Set CourseId)
, matchingLogRunCoursesExcluded :: Set CourseId
, matchingLogMatchingsExcluded :: Map (UserId, CourseId) (NonNull (Set MatchingExcludedReason))
, matchingLogRunLog :: Seq (MatchingLog UserId CourseId Natural)
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving anyclass (NFData)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 3
} ''MatchingLogRun
allocationDone :: AllocationId -> DB (Maybe UTCTime) allocationStarted :: AllocationId -> DB (Maybe UTCTime)
allocationDone allocId = fmap (E.unValue <=< listToMaybe) . E.select . E.from $ \participant -> do -- ^ Time the first allocation was made
E.where_ $ participant E.^. CourseParticipantAllocated E.==. E.just (E.val allocId) allocationStarted allocId = fmap (E.unValue <=< listToMaybe) . E.select . E.from $ \allocationMatching -> do
return . E.max_ $ participant E.^. CourseParticipantRegistration E.where_ $ allocationMatching E.^. AllocationMatchingAllocation E.==. E.val allocId
return . E.min_ $ allocationMatching E.^. AllocationMatchingTime
ordinalPriorities :: Monad m => ConduitT UserMatriculation (Map UserMatriculation AllocationPriority) m () ordinalPriorities :: Monad m => ConduitT UserMatriculation (Map UserMatriculation AllocationPriority) m ()
@ -60,7 +85,7 @@ computeAllocation :: AllocationId
-> Maybe (Set CourseId) -- ^ Optionally restrict allocation to only consider the given courses -> Maybe (Set CourseId) -- ^ Optionally restrict allocation to only consider the given courses
-> DB ( AllocationFingerprint -> DB ( AllocationFingerprint
, Set (UserId, CourseId) , Set (UserId, CourseId)
, Seq (MatchingLog UserId CourseId Natural) , Seq MatchingLogRun
) )
computeAllocation allocId cRestr = do computeAllocation allocId cRestr = do
allocations <- selectList [ CourseParticipantAllocated ==. Just allocId ] [] allocations <- selectList [ CourseParticipantAllocated ==. Just allocId ] []
@ -106,20 +131,36 @@ computeAllocation allocId cRestr = do
let capacities = Map.filter (maybe True (> 0)) . Map.fromList $ (view (_1 . _entityVal . _allocationCourseCourse) &&& view (_2 . _Value)) <$> courses' let capacities = Map.filter (maybe True (> 0)) . Map.fromList $ (view (_1 . _entityVal . _allocationCourseCourse) &&& view (_2 . _Value)) <$> courses'
applications' <- selectList [ CourseApplicationAllocation ==. Just allocId ] [] applications' <- selectList [ CourseApplicationAllocation ==. Just allocId ] []
let alreadyAssigned (Entity _ CourseApplication{..}) = orM excludedMatchings <- flip execStateT mempty . forM_ applications' $ \(Entity _ CourseApplication{..}) -> do
[ exists [ TransactionLogInfo ==. toJSON (TransactionCourseParticipantDeleted courseApplicationCourse courseApplicationUser) ] let
, exists [ CourseParticipantCourse ==. courseApplicationCourse, CourseParticipantUser ==. courseApplicationUser ] tellExcluded :: MatchingExcludedReason -> StateT _ _ ()
] tellExcluded reason = State.modify' $ Map.insertWith (<>) (courseApplicationUser, courseApplicationCourse) (opoint reason :: NonNull (Set MatchingExcludedReason))
applications'' <- applications'
& filter ((\CourseApplication{..} -> not courseApplicationRatingVeto && fromMaybe True (courseApplicationRatingPoints ^? _Just . passingGrade . _Wrapped)) . entityVal) when (courseApplicationRatingVeto || maybe False not (courseApplicationRatingPoints ^? _Just . passingGrade . _Wrapped)) $
& filterM (fmap not . alreadyAssigned) tellExcluded MatchingExcludedVeto
allocStarted <- lift $ allocationStarted allocId
whenIsJust allocStarted $ \allocStarted' -> do
let partDeleted = lift $ exists [ TransactionLogInfo ==. toJSON (TransactionCourseParticipantDeleted courseApplicationCourse courseApplicationUser), TransactionLogTime >=. allocStarted' ]
whenM partDeleted $
tellExcluded MatchingExcludedParticipationExisted
let partExists :: StateT _ DB Bool
partExists = lift $ exists [ CourseParticipantCourse ==. courseApplicationCourse, CourseParticipantUser ==. courseApplicationUser ]
whenM partExists $
tellExcluded MatchingExcludedParticipationExists
let lecturerExists = lift $ exists [ LecturerCourse ==. courseApplicationCourse, LecturerUser ==. courseApplicationUser ]
whenM lecturerExists $
tellExcluded MatchingExcludedLecturer
let applications'' = applications'
& map entityVal
& filter (\CourseApplication{..} -> Map.notMember (courseApplicationUser, courseApplicationCourse) excludedMatchings)
let preferences = Map.fromList $ do let preferences = Map.fromList $ do
Entity _ CourseApplication{..} <- applications'' CourseApplication{..} <- applications''
guard $ Map.member courseApplicationCourse capacities guard $ Map.member courseApplicationCourse capacities
return ((courseApplicationUser, courseApplicationCourse), (courseApplicationAllocationPriority, courseApplicationRatingPoints)) return ((courseApplicationUser, courseApplicationCourse), (courseApplicationAllocationPriority, courseApplicationRatingPoints))
$logErrorS "computeAllocation" $ tshow preferences
gradeScale <- getsYesod $ view _appAllocationGradeScale gradeScale <- getsYesod $ view _appAllocationGradeScale
gradeOrdinalProportion <- getsYesod $ view _appAllocationGradeOrdinalProportion gradeOrdinalProportion <- getsYesod $ view _appAllocationGradeOrdinalProportion
let ordinalUsers = getSum . flip foldMap users'' $ \(_, prio) -> case prio of let ordinalUsers = getSum . flip foldMap users'' $ \(_, prio) -> case prio of
@ -155,23 +196,28 @@ computeAllocation allocId cRestr = do
fingerprint :: AllocationFingerprint fingerprint :: AllocationFingerprint
fingerprint = Crypto.hash . toStrict $ Binary.encode (users'', capacities, preferences, gradeScale, gradeOrdinalPlaces) fingerprint = Crypto.hash . toStrict $ Binary.encode (users'', capacities, preferences, gradeScale, gradeOrdinalPlaces)
g = mkStdGen $ hash (BA.convert fingerprint :: ByteString) g = mkStdGen $ hash fingerprint
let let
doAllocationWithout cs = runWriter $ computeMatchingLog g cloneCounts capacities' preferences' centralNudge doAllocationWithout :: Set CourseId -> Writer (Seq (MatchingLog UserId CourseId Natural)) (Set (UserId, CourseId))
doAllocationWithout cs = computeMatchingLog g cloneCounts capacities' preferences' centralNudge
where where
capacities' = Map.filterWithKey (\ c _ -> Set.notMember c cs) capacities capacities' = Map.filterWithKey (\ c _ -> Set.notMember c cs) capacities
preferences' = Map.filterWithKey (\(_, c) _ -> Set.notMember c cs) preferences preferences' = Map.filterWithKey (\(_, c) _ -> Set.notMember c cs) preferences
allocationLoop cs allocationLoop :: Set CourseId -> Writer (Seq MatchingLogRun) (Set (UserId, CourseId))
| not $ null belowMin = doAllocationWithout $ cs <> Set.fromList belowMin allocationLoop cs = do
| otherwise = (allocs, mLog) allocs <- mapWriter (over _2 $ pure . MatchingLogRun cRestr cs excludedMatchings) $ doAllocationWithout cs
where let
(allocs, mLog) = doAllocationWithout cs belowMin = catMaybes . flip map courses' $ \(Entity _ AllocationCourse{..}, _, E.Value minCap) -> do
belowMin = catMaybes . flip map courses' $ \(Entity _ AllocationCourse{..}, _, E.Value minCap) -> guard . not $ Set.member allocationCourseCourse cs
guardOn (Set.size (Set.filter (\(_, c) -> c == allocationCourseCourse) allocs) < minCap) allocationCourseCourse guard $ Set.size (Set.filter (\(_, c) -> c == allocationCourseCourse) allocs) < minCap
return allocationCourseCourse
if
| not $ null belowMin -> allocationLoop $ cs <> Set.fromList belowMin
| otherwise -> return allocs
return . (\(ms, mLog) -> (fingerprint, ms, mLog)) $!! allocationLoop Set.empty return . (\(ms, mLog) -> (fingerprint, ms, mLog)) $!! runWriter (allocationLoop Set.empty)
doAllocation :: AllocationId doAllocation :: AllocationId
@ -183,22 +229,30 @@ doAllocation allocId now regs =
mField <- (courseApplicationField . entityVal =<<) . listToMaybe <$> selectList [CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid, CourseApplicationAllocation ==. Just allocId] [] mField <- (courseApplicationField . entityVal =<<) . listToMaybe <$> selectList [CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid, CourseApplicationAllocation ==. Just allocId] []
void . insertUnique $ CourseParticipant cid uid now mField (Just allocId) void . insertUnique $ CourseParticipant cid uid now mField (Just allocId)
ppMatchingLog :: forall mono. ppMatchingLog :: Seq MatchingLogRun -> Text
( MonoFoldable mono ppMatchingLog = unlines . map prettyRun . otoList
, Element mono ~ MatchingLog UserId CourseId Natural
)
=> mono -> Text
ppMatchingLog = unlines . map (tshow . pretty) . otoList
where where
prettyRun MatchingLogRun{..} = unlines
[ "----- STARTING RUN -----"
, "Course restriction: " <> tshow (Set.toAscList <$> matchingLogRunCourseRestriction)
, "Courses excluded: " <> tshow (Set.toAscList matchingLogRunCoursesExcluded)
, "Matchings excluded (user, course): "
, unlines . map (" " <>) . flip ifoldMap matchingLogMatchingsExcluded $ \(uid, cid) (otoList -> reasons) -> pure $
"(" <> tshow (fromSqlKey uid) <> ", " <> tshow (fromSqlKey cid) <> ") " <> intercalate ", " (map tshow reasons) :: [Text]
, "------------------------"
, unlines . map (tshow . pretty) $ otoList matchingLogRunLog
, "------ RUN ENDED -------"
]
pretty :: MatchingLog UserId CourseId Natural -> MatchingLog Int64 Int64 Natural pretty :: MatchingLog UserId CourseId Natural -> MatchingLog Int64 Int64 Natural
pretty = over (param @1) fromSqlKey pretty = over (param @1) fromSqlKey
. over (param @2) fromSqlKey . over (param @2) fromSqlKey
storeAllocationResult :: AllocationId storeAllocationResult :: AllocationId
-> UTCTime -> UTCTime
-> (AllocationFingerprint, Set (UserId, CourseId), Seq (MatchingLog UserId CourseId Natural)) -> (AllocationFingerprint, Set (UserId, CourseId), Seq MatchingLogRun)
-> DB () -> DB ()
storeAllocationResult allocId now (allocFp, allocMatchings, ppMatchingLog -> allocLog) = do storeAllocationResult allocId now (allocFp, allocMatchings, ppMatchingLog -> allocLog) = do
insert_ . AllocationMatching allocId allocFp <=< insert $ File "matchings.log" (Just $ encodeUtf8 allocLog) now insert_ . AllocationMatching allocId allocFp now <=< insert $ File "matchings.log" (Just $ encodeUtf8 allocLog) now
doAllocation allocId now allocMatchings doAllocation allocId now allocMatchings

View File

@ -16,7 +16,6 @@ import Data.Time.Zones
import Data.Time.Clock.POSIX import Data.Time.Clock.POSIX
import Handler.Utils.DateTime import Handler.Utils.DateTime
import Handler.Utils.Allocation (allocationDone)
import Control.Monad.Writer.Class (MonadWriter(..)) import Control.Monad.Writer.Class (MonadWriter(..))
@ -379,7 +378,9 @@ determineCrontab = execWriterT $ do
} }
_other _other
-> return () -> return ()
doneSince <- lift $ allocationDone nAllocation doneSince <- lift $ fmap (E.unValue <=< listToMaybe) . E.select . E.from $ \participant -> do
E.where_ $ participant E.^. CourseParticipantAllocated E.==. E.just (E.val nAllocation)
return . E.max_ $ participant E.^. CourseParticipantRegistration
whenIsJust doneSince $ \doneSince' -> whenIsJust doneSince $ \doneSince' ->
tell $ HashMap.singleton tell $ HashMap.singleton

View File

@ -620,6 +620,10 @@ customMigrations = Map.fromListWith (>>)
ALTER TABLE "external_exam" ALTER COLUMN "grading_mode" SET NOT NULL; ALTER TABLE "external_exam" ALTER COLUMN "grading_mode" SET NOT NULL;
|] |]
) )
, ( AppliedMigrationKey [migrationVersion|33.0.0|] [version|34.0.0|]
, whenM (tableExists "allocation_matching") $
tableDropEmpty "allocation_matching"
)
] ]

View File

@ -48,8 +48,8 @@ $newline never
<th .table__th> <th .table__th>
_{MsgAllocationMatchedUsers} _{MsgAllocationMatchedUsers}
<tbody> <tbody>
$forall ((Entity _ AllocationCourse{allocationCourseMinCapacity}, Entity _ Course{courseTerm, courseSchool, courseName, courseCapacity, courseShorthand}, participants), allocated) <- allocationCourses' $forall ((Entity _ AllocationCourse{allocationCourseMinCapacity}, Entity cid Course{courseTerm, courseSchool, courseName, courseCapacity, courseShorthand}, participants), allocated) <- allocationCourses'
<tr .table__row> <tr .table__row :member cid matchingLogRunCoursesExcluded:.allocation-course--excluded>
$if showTerms $if showTerms
<td .table__td> <td .table__td>
<div .table__td-content> <div .table__td-content>