refactor(invitations): cleanup

This commit is contained in:
Gregor Kleen 2019-09-06 09:30:14 +02:00
parent baa7a52cdb
commit 5fb6910a58
6 changed files with 72 additions and 32 deletions

View File

@ -60,7 +60,7 @@ data AllocationCourseForm = AllocationCourseForm
, acfMinCapacity :: Int , acfMinCapacity :: Int
} }
courseToForm :: Entity Course -> [Lecturer] -> [(UserEmail, InvitationDBData Lecturer)] -> Maybe (Entity AllocationCourse) -> CourseForm courseToForm :: Entity Course -> [Lecturer] -> Map UserEmail (InvitationDBData Lecturer) -> Maybe (Entity AllocationCourse) -> CourseForm
courseToForm (Entity cid Course{..}) lecs lecInvites alloc = CourseForm courseToForm (Entity cid Course{..}) lecs lecInvites alloc = CourseForm
{ cfCourseId = Just cid { cfCourseId = Just cid
, cfName = courseName , cfName = courseName
@ -83,7 +83,7 @@ courseToForm (Entity cid Course{..}) lecs lecInvites alloc = CourseForm
, cfRegTo = courseRegisterTo , cfRegTo = courseRegisterTo
, cfDeRegUntil = courseDeregisterUntil , cfDeRegUntil = courseDeregisterUntil
, cfLecturers = [Right (lecturerUser, lecturerType) | Lecturer{..} <- lecs] , cfLecturers = [Right (lecturerUser, lecturerType) | Lecturer{..} <- lecs]
++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- lecInvites ] ++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- Map.toList lecInvites ]
} }
where where
cfAppInstructionFiles = Just . transPipe runDB $ selectAppFiles .| C.map (Left . E.unValue) cfAppInstructionFiles = Just . transPipe runDB $ selectAppFiles .| C.map (Left . E.unValue)
@ -397,7 +397,7 @@ getCourseNewR = do
return course return course
template <- case listToMaybe oldCourses of template <- case listToMaybe oldCourses of
(Just oldTemplate) -> (Just oldTemplate) ->
let newTemplate = courseToForm oldTemplate [] [] Nothing in let newTemplate = courseToForm oldTemplate mempty mempty Nothing in
return $ Just $ newTemplate return $ Just $ newTemplate
{ cfCourseId = Nothing { cfCourseId = Nothing
, cfTerm = TermKey $ TermIdentifier 0 Winter -- invalid, will be ignored; undefined won't work due to strictness , cfTerm = TermKey $ TermIdentifier 0 Winter -- invalid, will be ignored; undefined won't work due to strictness
@ -429,7 +429,7 @@ pgCEditR tid ssh csh = do
courseData <- runDB $ do courseData <- runDB $ do
mbCourse <- getBy (TermSchoolCourseShort tid ssh csh) mbCourse <- getBy (TermSchoolCourseShort tid ssh csh)
mbLecs <- for mbCourse $ \course -> map entityVal <$> selectList [LecturerCourse ==. entityKey course] [Asc LecturerType] mbLecs <- for mbCourse $ \course -> map entityVal <$> selectList [LecturerCourse ==. entityKey course] [Asc LecturerType]
mbLecInvites <- for mbCourse $ sourceInvitationsList . entityKey mbLecInvites <- for mbCourse $ sourceInvitationsF . entityKey
mbAllocation <- for mbCourse $ \course -> getBy . UniqueAllocationCourse $ entityKey course mbAllocation <- for mbCourse $ \course -> getBy . UniqueAllocationCourse $ entityKey course
return $ (,,,) <$> mbCourse <*> mbLecs <*> mbLecInvites <*> mbAllocation return $ (,,,) <$> mbCourse <*> mbLecs <*> mbLecInvites <*> mbAllocation
-- IMPORTANT: both GET and POST Handler must use the same template, -- IMPORTANT: both GET and POST Handler must use the same template,

View File

@ -8,12 +8,13 @@ module Handler.Exam.Form
) where ) where
import Import import Import
import Handler.Exam.CorrectorInvite import Handler.Exam.CorrectorInvite ()
import Handler.Utils import Handler.Utils
import Handler.Utils.Invitations import Handler.Utils.Invitations
import Data.Map ((!)) import Data.Map ((!))
import qualified Data.Map as Map
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
@ -231,7 +232,7 @@ examFormTemplate (Entity eId Exam{..}) = do
examParts <- selectList [ ExamPartExam ==. eId ] [] examParts <- selectList [ ExamPartExam ==. eId ] []
occurrences <- selectList [ ExamOccurrenceExam ==. eId ] [] occurrences <- selectList [ ExamOccurrenceExam ==. eId ] []
correctors <- selectList [ ExamCorrectorExam ==. eId ] [] correctors <- selectList [ ExamCorrectorExam ==. eId ] []
invitations <- map (\(email, InvDBDataExamCorrector) -> email) <$> sourceInvitationsList eId invitations <- Map.keysSet <$> sourceInvitationsF @ExamCorrector eId
examParts' <- forM examParts $ \(Entity pid part) -> (,) <$> encrypt pid <*> pure part examParts' <- forM examParts $ \(Entity pid part) -> (,) <$> encrypt pid <*> pure part
occurrences' <- forM occurrences $ \(Entity oid occ) -> (,) <$> encrypt oid <*> pure occ occurrences' <- forM occurrences $ \(Entity oid occ) -> (,) <$> encrypt oid <*> pure occ
@ -273,7 +274,7 @@ examFormTemplate (Entity eId Exam{..}) = do
, epfWeight = examPartWeight , epfWeight = examPartWeight
} }
, efCorrectors = Set.unions , efCorrectors = Set.unions
[ Set.fromList $ map Left invitations [ Set.mapMonotonic Left invitations
, Set.fromList . map Right $ do , Set.fromList . map Right $ do
Entity _ ExamCorrector{..} <- correctors Entity _ ExamCorrector{..} <- correctors
return examCorrectorUser return examCorrectorUser

View File

@ -704,7 +704,7 @@ correctorForm shid = wFormToAForm $ do
currentLoads :: DB Loads currentLoads :: DB Loads
currentLoads = Map.union currentLoads = Map.union
<$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (sheetCorrectorState, sheetCorrectorLoad)) (selectList [ SheetCorrectorSheet ==. shid ] []) <$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (sheetCorrectorState, sheetCorrectorLoad)) (selectList [ SheetCorrectorSheet ==. shid ] [])
<*> fmap (foldMap $ \(email, InvDBDataSheetCorrector load state) -> Map.singleton (Left email) (state, load)) (sourceInvitationsList shid) <*> fmap (fmap ((,) <$> invDBSheetCorrectorState <*> invDBSheetCorrectorLoad) . Map.mapKeysMonotonic Left) (sourceInvitationsF shid)
(defaultLoads', currentLoads') <- liftHandlerT . runDB $ (,) <$> defaultLoads shid <*> currentLoads (defaultLoads', currentLoads') <- liftHandlerT . runDB $ (,) <$> defaultLoads shid <*> currentLoads
isWrite <- liftHandlerT $ isWriteRequest currentRoute isWrite <- liftHandlerT $ isWriteRequest currentRoute

View File

@ -330,7 +330,7 @@ submissionHelper tid ssh csh shn mcid = do
| uid == userID = (Any True , mempty ) | uid == userID = (Any True , mempty )
| otherwise = (mempty , Set.singleton $ Right userID) | otherwise = (mempty , Set.singleton $ Right userID)
invites <- sourceInvitationsList smid <&> Set.fromList . map (\(email, InvDBDataSubmissionUser) -> Left email) invites <- sourceInvitationsF smid <&> Set.fromList . map (\(email, InvDBDataSubmissionUser) -> Left email)
return . over _2 (Set.union invites) $ foldMap breakUserFromBuddies submittors return . over _2 (Set.union invites) $ foldMap breakUserFromBuddies submittors
@ -440,6 +440,12 @@ submissionHelper tid ssh csh shn mcid = do
| isJust msmid -> setOf (folded . _entityVal . _submissionUserUser . to Right) <$> selectList [SubmissionUserSubmission ==. smid] [] | isJust msmid -> setOf (folded . _entityVal . _submissionUserUser . to Right) <$> selectList [SubmissionUserSubmission ==. smid] []
| otherwise -> return Set.empty -- optimization (do not perform selection if submission was freshly created) | otherwise -> return Set.empty -- optimization (do not perform selection if submission was freshly created)
-- Since invitations carry no data we only need to consider changes to
-- the set of users/invited emails
-- Otherwise we would have to update old invitations (via
-- `sinkInvitationsF`) because their associated @DBData@ might have
-- changed
forM_ (subUsers `setSymmDiff` subUsersOld) $ \change -> if forM_ (subUsers `setSymmDiff` subUsersOld) $ \change -> if
-- change is a new user being added to the submission users => send invitation / insert -- change is a new user being added to the submission users => send invitation / insert
| change `Set.member` subUsers -> case change of | change `Set.member` subUsers -> case change of
@ -449,11 +455,11 @@ submissionHelper tid ssh csh shn mcid = do
return () return ()
Right subUid -> do Right subUid -> do
-- user exists and has an id => insert as SubmissionUser and audit -- user exists and has an id => insert as SubmissionUser and audit
_ <- insert $ SubmissionUser subUid smid insert_ $ SubmissionUser subUid smid
audit $ TransactionSubmissionUserEdit smid subUid audit $ TransactionSubmissionUserEdit smid subUid
-- change is an old user that is not a submission user anymore => delete invitation / delete -- change is an old user that is not a submission user anymore => delete invitation / delete
| otherwise -> case change of | otherwise -> case change of
Left subEmail -> runConduit $ yield subEmail .| deleteInvitations @SubmissionUser smid Left subEmail -> deleteInvitation @SubmissionUser smid subEmail
Right subUid -> do Right subUid -> do
deleteWhere [SubmissionUserUser ==. subUid] deleteWhere [SubmissionUserUser ==. subUid]
audit $ TransactionSubmissionUserDelete smid subUid audit $ TransactionSubmissionUserDelete smid subUid

View File

@ -413,7 +413,7 @@ postTEditR tid ssh csh tutn = do
E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid
return $ tutor E.^. TutorUser return $ tutor E.^. TutorUser
tutorInvites <- sourceInvitationsList tutid tutorInvites <- sourceInvitationsF @Tutor tutid
let let
template = TutorialForm template = TutorialForm
@ -427,7 +427,7 @@ postTEditR tid ssh csh tutn = do
, tfRegisterTo = tutorialRegisterTo , tfRegisterTo = tutorialRegisterTo
, tfDeregisterUntil = tutorialDeregisterUntil , tfDeregisterUntil = tutorialDeregisterUntil
, tfTutors = Set.fromList (map Right tutorIds) , tfTutors = Set.fromList (map Right tutorIds)
<> Set.fromList (map (\(email, InvDBDataTutor) -> Left email) tutorInvites) <> Set.mapMonotonic Left (Map.keysSet tutorInvites)
} }
return (cid, tutid, template) return (cid, tutid, template)

View File

@ -10,9 +10,9 @@ module Handler.Utils.Invitations
, _invitationDBData, _invitationTokenData , _invitationDBData, _invitationTokenData
, InvitationReference(..), invRef , InvitationReference(..), invRef
, InvitationConfig(..), InvitationTokenConfig(..) , InvitationConfig(..), InvitationTokenConfig(..)
, sourceInvitations, sourceInvitationsList , sourceInvitations, sourceInvitationsF
, deleteInvitations , deleteInvitations, deleteInvitationsF, deleteInvitation
, sinkInvitations, sinkInvitationsF , sinkInvitations, sinkInvitationsF, sinkInvitation
, invitationR', InvitationR(..) , invitationR', InvitationR(..)
) where ) where
@ -68,8 +68,6 @@ class ( PersistRecordBackend junction (YesodPersistBackend UniWorX)
_InvitationData = id _InvitationData = id
-- | If `ephemeralInvitation` is not `Nothing` pending invitations are not stored in the database -- | If `ephemeralInvitation` is not `Nothing` pending invitations are not stored in the database
--
-- In this case no invitation data can be stored in the database (@InvitationDBData junction ~ ()@)
ephemeralInvitation :: Maybe (AnIso' () (InvitationDBData junction)) ephemeralInvitation :: Maybe (AnIso' () (InvitationDBData junction))
ephemeralInvitation = Nothing ephemeralInvitation = Nothing
@ -170,13 +168,11 @@ sinkInvitations :: forall junction.
IsInvitableJunction junction IsInvitableJunction junction
=> InvitationConfig junction => InvitationConfig junction
-> Sink (Invitation' junction) (YesodJobDB UniWorX) () -> Sink (Invitation' junction) (YesodJobDB UniWorX) ()
-- | Register invitations in the database -- | Register invitations in the database and send them by email
-- --
-- When an invitation for a certain junction (i.e. an `UserEmail`, `Key -- When an invitation for a certain junction (i.e. an `UserEmail`, `Key
-- (InvitationFor junction)`-Pair) already exists it's `InvitationData` is -- (InvitationFor junction)`-Pair) already exists it is deleted and resent
-- updated, instead. -- (because the token-data may have changed)
--
-- For new junctions an invitation is sent by e-mail.
sinkInvitations InvitationConfig{..} = determineExists .| sinkInvitations' sinkInvitations InvitationConfig{..} = determineExists .| sinkInvitations'
where where
determineExists :: Conduit (Invitation' junction) determineExists :: Conduit (Invitation' junction)
@ -242,6 +238,13 @@ sinkInvitationsF :: forall junction mono.
-- | Non-conduit version of `sinkInvitations` -- | Non-conduit version of `sinkInvitations`
sinkInvitationsF cfg invs = runConduit $ mapM_ yield invs .| sinkInvitations cfg sinkInvitationsF cfg invs = runConduit $ mapM_ yield invs .| sinkInvitations cfg
sinkInvitation :: forall junction.
IsInvitableJunction junction
=> InvitationConfig junction
-> Invitation' junction
-> YesodJobDB UniWorX ()
-- | Singular version of `sinkInvitationsF`
sinkInvitation cfg = sinkInvitationsF cfg . Identity
sourceInvitations :: forall junction. sourceInvitations :: forall junction.
@ -255,23 +258,53 @@ sourceInvitations forKey = selectSource [InvitationFor ==. invRef @junction forK
JSON.Success dbData -> return (invitationEmail, dbData) JSON.Success dbData -> return (invitationEmail, dbData)
JSON.Error str -> fail $ "Could not decode invitationData: " <> str JSON.Error str -> fail $ "Could not decode invitationData: " <> str
sourceInvitationsList :: forall junction. sourceInvitationsF :: forall junction map.
IsInvitableJunction junction ( IsInvitableJunction junction
=> Key (InvitationFor junction) , IsMap map
-> YesodDB UniWorX [(UserEmail, InvitationDBData junction)] , ContainerKey map ~ UserEmail
sourceInvitationsList forKey = runConduit $ sourceInvitations forKey .| C.foldMap pure , MapValue map ~ InvitationDBData junction
)
=> Key (InvitationFor junction)
-> YesodDB UniWorX map
sourceInvitationsF forKey = runConduit $ sourceInvitations forKey .| C.foldMap (uncurry singletonMap)
-- | Deletes all invitations for given emails and a given junction. (Type application required) -- | Deletes all invitations for given emails and a given instance of the
-- non-user side of the junction
--
-- Requires type application to determine @junction@-type, i.e.:
--
-- > runConduit $ yield userEmail .| deleteInvitations @SubmissionUser submissionId
deleteInvitations :: forall junction m. deleteInvitations :: forall junction m.
( IsInvitableJunction junction ( IsInvitableJunction junction
, MonadIO m , MonadIO m
) )
=> Key (InvitationFor junction) => Key (InvitationFor junction)
-> Sink UserEmail (ReaderT SqlBackend m) () -> Sink UserEmail (ReaderT SqlBackend m) ()
deleteInvitations k = do deleteInvitations k = C.foldMap Set.singleton >>= lift . deleteInvitationsF @junction k
subEmails <- C.foldMap Set.singleton
lift $ deleteWhere [InvitationEmail <-. Set.toList subEmails, InvitationFor ==. invRef @junction k] deleteInvitationsF :: forall junction m mono.
( IsInvitableJunction junction
, MonadIO m
, MonoFoldable mono
, Element mono ~ UserEmail
)
=> Key (InvitationFor junction)
-> mono
-> ReaderT SqlBackend m ()
-- | Non-conduit version of `deleteInvitations`
deleteInvitationsF invitationFor (otoList -> emailList)
= deleteWhere [InvitationEmail <-. nub emailList, InvitationFor ==. invRef @junction invitationFor]
deleteInvitation :: forall junction m.
( IsInvitableJunction junction
, MonadIO m
)
=> Key (InvitationFor junction)
-> UserEmail
-> ReaderT SqlBackend m ()
-- | Singular version of `deleteInvitationsF`
deleteInvitation invitationFor = deleteInvitationsF @junction invitationFor . Identity
data ButtonInvite = BtnInviteAccept | BtnInviteDecline data ButtonInvite = BtnInviteAccept | BtnInviteDecline