feat(submission-groups): invite w/ submission-group & audit

This commit is contained in:
Gregor Kleen 2020-04-28 15:49:20 +02:00
parent 52670bc905
commit 7f10d44aee
6 changed files with 100 additions and 30 deletions

View File

@ -178,6 +178,16 @@ data Transaction
, transactionUser :: UserId , transactionUser :: UserId
} }
| TransactionSubmissionGroupSet
{ transactionCourse :: CourseId
, transactionUser :: UserId
, transactionSubmissionGroup :: SubmissionGroupName
}
| TransactionSubmissionGroupUnset
{ transactionCourse :: CourseId
, transactionUser :: UserId
}
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions deriveJSON defaultOptions

View File

@ -7,6 +7,7 @@ module Handler.Course.ParticipantInvite
, AddParticipantsResult(..) , AddParticipantsResult(..)
, addParticipantsResultMessages , addParticipantsResultMessages
, registerUsers, registerUser , registerUsers, registerUser
, registerUsers', registerUser'
) where ) where
import Import import Import
@ -14,10 +15,12 @@ import Import
import Utils.Form import Utils.Form
import Handler.Utils import Handler.Utils
import Handler.Utils.Invitations import Handler.Utils.Invitations
import Handler.Utils.Course
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Map as Map
import Jobs.Queue import Jobs.Queue
@ -44,6 +47,8 @@ instance IsInvitableJunction CourseParticipant where
-- no data needed in DB to manage participant invitation -- no data needed in DB to manage participant invitation
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
data InvitationTokenData CourseParticipant = InvTokenDataParticipant data InvitationTokenData CourseParticipant = InvTokenDataParticipant
{ invTokenParticipantSubmissionGroup :: Maybe SubmissionGroupName
}
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
_InvitableJunction = iso _InvitableJunction = iso
@ -63,10 +68,10 @@ instance FromJSON (InvitationDBData CourseParticipant) where
parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 3 } parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 3 }
instance ToJSON (InvitationTokenData CourseParticipant) where instance ToJSON (InvitationTokenData CourseParticipant) where
toJSON = genericToJSON defaultOptions { constructorTagModifier = camelToPathPiece' 3 } toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 3, omitNothingFields = True }
toEncoding = genericToEncoding defaultOptions { constructorTagModifier = camelToPathPiece' 3 } toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 3, omitNothingFields = True }
instance FromJSON (InvitationTokenData CourseParticipant) where instance FromJSON (InvitationTokenData CourseParticipant) where
parseJSON = genericParseJSON defaultOptions { constructorTagModifier = camelToPathPiece' 3 } parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 3, omitNothingFields = True }
participantInvitationConfig :: InvitationConfig CourseParticipant participantInvitationConfig :: InvitationConfig CourseParticipant
participantInvitationConfig = InvitationConfig{..} participantInvitationConfig = InvitationConfig{..}
@ -91,9 +96,10 @@ participantInvitationConfig = InvitationConfig{..}
studyFeatures <- wreq (studyFeaturesFieldFor Nothing False [] $ Just uid) studyFeatures <- wreq (studyFeaturesFieldFor Nothing False [] $ Just uid)
(fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) Nothing (fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) Nothing
return . fmap (, ()) $ JunctionParticipant <$> pure now <*> studyFeatures <*> pure Nothing return . fmap (, ()) $ JunctionParticipant <$> pure now <*> studyFeatures <*> pure Nothing
invitationInsertHook _ _ _ CourseParticipant{..} _ act = do invitationInsertHook _ _ (_, InvTokenDataParticipant{..}) CourseParticipant{..} _ act = do
res <- act res <- act
audit $ TransactionCourseParticipantEdit courseParticipantCourse courseParticipantUser audit $ TransactionCourseParticipantEdit courseParticipantCourse courseParticipantUser
void $ setUserSubmissionGroup courseParticipantCourse courseParticipantUser invTokenParticipantSubmissionGroup
return res return res
invitationSuccessMsg (Entity _ Course{..}) _ = invitationSuccessMsg (Entity _ Course{..}) _ =
return . SomeMessage $ MsgCourseParticipantInvitationAccepted (CI.original courseName) return . SomeMessage $ MsgCourseParticipantInvitationAccepted (CI.original courseName)
@ -118,11 +124,17 @@ postCAddUserR tid ssh csh = do
cid <- runDB . getKeyBy404 $ TermSchoolCourseShort tid ssh csh cid <- runDB . getKeyBy404 $ TermSchoolCourseShort tid ssh csh
((usersToEnlist,formWgt),formEncoding) <- runFormPost . renderWForm FormStandard $ do ((usersToEnlist,formWgt),formEncoding) <- runFormPost . renderWForm FormStandard $ do
enlist <- wreq checkBoxField (fslI MsgCourseParticipantEnlistDirectly) (Just False) enlist <- wreq checkBoxField (fslI MsgCourseParticipantEnlistDirectly) (Just False)
wreq (multiUserField (maybe True not $ formResultToMaybe enlist) Nothing)
(fslI MsgCourseParticipantInviteField & setTooltip MsgMultiEmailFieldTip) Nothing let submissionGroupOpts = optionsPersist [SubmissionGroupCourse ==. cid] [Asc SubmissionGroupName] submissionGroupName <&> fmap (submissionGroupName . entityVal)
mbGrp <- wopt (textField & cfStrip & cfCI & addDatalist submissionGroupOpts) (fslI MsgSubmissionGroup & setTooltip MsgSubmissionGroupEmptyIsUnsetTip) Nothing
users <- wreq (multiUserField (maybe True not $ formResultToMaybe enlist) Nothing)
(fslI MsgCourseParticipantInviteField & setTooltip MsgMultiEmailFieldTip) Nothing
return $ Map.fromSet . const <$> mbGrp <*> users
formResultModal usersToEnlist (CourseR tid ssh csh CUsersR) $ formResultModal usersToEnlist (CourseR tid ssh csh CUsersR) $
hoist runDBJobs . registerUsers cid hoist runDBJobs . registerUsers' cid
let heading = prependCourseTitle tid ssh csh MsgCourseParticipantsRegisterHeading let heading = prependCourseTitle tid ssh csh MsgCourseParticipantsRegisterHeading
@ -135,13 +147,16 @@ postCAddUserR tid ssh csh = do
} }
registerUsers :: CourseId -> Set (Either UserEmail UserId) -> WriterT [Message] (YesodJobDB UniWorX) () registerUsers :: CourseId -> Set (Either UserEmail UserId) -> WriterT [Message] (YesodJobDB UniWorX) ()
registerUsers cid users = do registerUsers cid = registerUsers' cid . Map.fromSet (const Nothing)
let (emails,uids) = partitionEithers $ Set.toList users
registerUsers' :: CourseId -> Map (Either UserEmail UserId) (Maybe SubmissionGroupName) -> WriterT [Message] (YesodJobDB UniWorX) ()
registerUsers' cid users = do
let (emails,uids) = partitionKeysEither users
-- send Invitation eMails to unkown users -- send Invitation eMails to unkown users
lift $ sinkInvitationsF participantInvitationConfig [(mail,cid,(InvDBDataParticipant,InvTokenDataParticipant)) | mail <- emails] lift $ sinkInvitationsF participantInvitationConfig [(mail,cid,(InvDBDataParticipant,InvTokenDataParticipant{..})) | (mail, invTokenParticipantSubmissionGroup) <- Map.toList emails]
-- register known users -- register known users
tell <=< lift . addParticipantsResultMessages <=< lift . execWriterT $ mapM_ (registerUser cid) uids tell <=< lift . addParticipantsResultMessages <=< lift . execWriterT $ imapM_ (registerUser' cid) uids
unless (null emails) $ unless (null emails) $
tell . pure <=< messageI Success . MsgCourseParticipantsInvited $ length emails tell . pure <=< messageI Success . MsgCourseParticipantsInvited $ length emails
@ -172,7 +187,13 @@ addParticipantsResultMessages AddParticipantsResult{..} = execWriterT $ do
registerUser :: CourseId registerUser :: CourseId
-> UserId -> UserId
-> WriterT AddParticipantsResult (YesodJobDB UniWorX) () -> WriterT AddParticipantsResult (YesodJobDB UniWorX) ()
registerUser cid uid = exceptT tell tell $ do registerUser cid uid = registerUser' cid uid Nothing
registerUser' :: CourseId
-> UserId
-> Maybe SubmissionGroupName
-> WriterT AddParticipantsResult (YesodJobDB UniWorX) ()
registerUser' cid uid mbGrp = exceptT tell tell $ do
whenM (lift . lift . existsBy $ UniqueParticipant uid cid) $ whenM (lift . lift . existsBy $ UniqueParticipant uid cid) $
throwError $ mempty { aurAlreadyRegistered = Set.singleton uid } throwError $ mempty { aurAlreadyRegistered = Set.singleton uid }
@ -198,6 +219,8 @@ registerUser cid uid = exceptT tell tell $ do
lift . lift . audit $ TransactionCourseParticipantEdit cid uid lift . lift . audit $ TransactionCourseParticipantEdit cid uid
lift . lift . queueDBJob . JobQueueNotification $ NotificationCourseRegistered uid cid lift . lift . queueDBJob . JobQueueNotification $ NotificationCourseRegistered uid cid
void . lift . lift $ setUserSubmissionGroup cid uid mbGrp
return $ case courseParticipantField of return $ case courseParticipantField of
Nothing -> mempty { aurNoUniquePrimaryField = Set.singleton uid } Nothing -> mempty { aurNoUniquePrimaryField = Set.singleton uid }
Just _ -> mempty { aurSuccess = Set.singleton uid } Just _ -> mempty { aurSuccess = Set.singleton uid }

View File

@ -11,6 +11,7 @@ import Import
import Utils.Form import Utils.Form
import Handler.Utils import Handler.Utils
import Handler.Utils.Course
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import Database.Esqueleto.Utils.TH import Database.Esqueleto.Utils.TH
@ -629,23 +630,13 @@ postCUsersR tid ssh csh = do
return mempty return mempty
addMessageI Success $ MsgCourseUsersExamRegistered nrReg addMessageI Success $ MsgCourseUsersExamRegistered nrReg
redirect $ CourseR tid ssh csh CUsersR redirect $ CourseR tid ssh csh CUsersR
(CourseUserSetSubmissionGroupData{ setSubmissionGroup = Just setSubmissionGroup }, selectedUsers) -> do (CourseUserSetSubmissionGroupData{..}, selectedUsers) -> do
Sum nrSet <- runDB $ do nrSet <- runDB $ setUsersSubmissionGroup cid selectedUsers setSubmissionGroup
Entity gId _ <- upsert (SubmissionGroup cid setSubmissionGroup) [ SubmissionGroupName =. setSubmissionGroup ]
flip foldMapM selectedUsers $ \uid -> case setSubmissionGroup of
fmap (maybe mempty . const $ Sum 1) . insertUnique $ SubmissionGroupUser gId uid Nothing -> addMessageI Success $ MsgCourseUsersSubmissionGroupUnset nrSet
addMessageI Success $ MsgCourseUsersSubmissionGroupSetNew nrSet Just _ -> addMessageI Success $ MsgCourseUsersSubmissionGroupSetNew nrSet
redirect $ CourseR tid ssh csh CUsersR
(CourseUserSetSubmissionGroupData{ setSubmissionGroup = Nothing }, selectedUsers) -> do
nrUnset <- runDB $ do
nrUnset <- E.deleteCount . E.from $ \submissionGroupUser ->
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser `E.in_` E.valList (Set.toList selectedUsers)
E.&&. E.subSelectForeign submissionGroupUser SubmissionGroupUserSubmissionGroup (E.^. SubmissionGroupCourse) E.==. E.val cid
E.delete . E.from $ \submissionGroup ->
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid
E.&&. E.not_ (E.exists . E.from $ \submissionGroupUser -> E.where_ $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId)
return nrUnset
addMessageI Success $ MsgCourseUsersSubmissionGroupUnset nrUnset
redirect $ CourseR tid ssh csh CUsersR redirect $ CourseR tid ssh csh CUsersR
let headingLong = [whamlet|_{MsgMenuCourseMembers} #{courseName course} #{tid}|] let headingLong = [whamlet|_{MsgMenuCourseMembers} #{courseName course} #{tid}|]

View File

@ -4,6 +4,8 @@ import Import
import Handler.Utils.Delete import Handler.Utils.Delete
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Data.Set as Set
courseDeleteRoute :: Set CourseId -> DeleteRoute Course courseDeleteRoute :: Set CourseId -> DeleteRoute Course
@ -27,3 +29,43 @@ courseDeleteRoute drRecords = DeleteRoute
, drSuccess = error "drSuccess undefined" , drSuccess = error "drSuccess undefined"
, drDelete = const id -- TODO: audit , drDelete = const id -- TODO: audit
} }
setUserSubmissionGroup :: ( MonadHandler m
, HandlerSite m ~ UniWorX
, backend ~ SqlBackend
, MonadCatch m
) => CourseId -> UserId -> Maybe SubmissionGroupName -> ReaderT backend m Bool
setUserSubmissionGroup cid uid = fmap (> 0) . setUsersSubmissionGroup cid (Set.singleton uid)
setUsersSubmissionGroup :: ( MonadHandler m
, HandlerSite m ~ UniWorX
, backend ~ SqlBackend
, MonadCatch m
) => CourseId -> Set UserId -> Maybe SubmissionGroupName -> ReaderT backend m Int64
setUsersSubmissionGroup cid uids Nothing = do
didDelete <- fmap getSum . flip foldMapM uids $ \uid -> do
didDelete <- fmap (> 0) . E.deleteCount . E.from $ \submissionGroupUser ->
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid
E.&&. E.subSelectForeign submissionGroupUser SubmissionGroupUserSubmissionGroup (E.^. SubmissionGroupCourse) E.==. E.val cid
when didDelete $
audit $ TransactionSubmissionGroupUnset cid uid
return $ bool mempty (Sum 1) didDelete
E.delete . E.from $ \submissionGroup ->
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid
E.&&. E.not_ (E.exists . E.from $ \submissionGroupUser -> E.where_ $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId)
return didDelete
setUsersSubmissionGroup cid uids (Just grp) = do
Entity gId _ <- upsert (SubmissionGroup cid grp) [ SubmissionGroupName =. grp ]
E.delete . E.from $ \submissionGroupUser ->
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser `E.in_` E.valList (Set.toList uids)
E.&&. E.subSelectForeign submissionGroupUser SubmissionGroupUserSubmissionGroup (E.^. SubmissionGroupCourse) E.==. E.val cid
E.&&. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.!=. E.val gId
E.delete . E.from $ \submissionGroup ->
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid
E.&&. E.not_ (E.exists . E.from $ \submissionGroupUser -> E.where_ $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId)
fmap getSum . flip foldMapM uids $ \uid -> do
didSet <- fmap (is _Just) . insertUnique $ SubmissionGroupUser gId uid
when didSet $
audit $ TransactionSubmissionGroupSet cid uid grp
return $ bool mempty (Sum 1) didSet

View File

@ -70,7 +70,7 @@ import Data.Ratio as Import ((%))
import Net.IP as Import (IP) import Net.IP as Import (IP)
import Database.Persist.Sql as Import (SqlReadT, SqlWriteT, fromSqlKey, toSqlKey) import Database.Persist.Sql as Import (SqlReadT, SqlWriteT, IsSqlBackend, fromSqlKey, toSqlKey)
import Ldap.Client.Pool as Import import Ldap.Client.Pool as Import

View File

@ -47,6 +47,7 @@ import qualified Data.Conduit.List as C
import Control.Lens import Control.Lens
import Control.Lens as Utils (none) import Control.Lens as Utils (none)
import Control.Lens.Extras (is)
import Data.Set.Lens import Data.Set.Lens
import Control.Arrow as Utils ((>>>)) import Control.Arrow as Utils ((>>>))
@ -481,6 +482,9 @@ assocsSet = setOf folded . imap (,)
mapF :: (Ord k, Finite k) => (k -> v) -> Map k v mapF :: (Ord k, Finite k) => (k -> v) -> Map k v
mapF = flip Map.fromSet $ Set.fromList universeF mapF = flip Map.fromSet $ Set.fromList universeF
partitionKeysEither :: Map (Either k1 k2) v -> (Map k1 v, Map k2 v)
partitionKeysEither = over _2 (Map.mapKeysMonotonic . view $ singular _Right) . over _1 (Map.mapKeysMonotonic . view $ singular _Left) . Map.partitionWithKey (\k _ -> is _Left k)
--------------- ---------------
-- Functions -- -- Functions --
--------------- ---------------