{-# OPTIONS_GHC -fno-warn-orphans #-} module Handler.ExamOffice.Users ( getEOUsersR, postEOUsersR , getEOUsersInviteR, postEOUsersInviteR ) where import Import import Utils.Form import Handler.Utils import Handler.Utils.Invitations import Text.Hamlet (ihamlet) import Data.Aeson hiding (Result(..)) import Jobs.Queue import qualified Database.Esqueleto as E import qualified Data.Set as Set import qualified Data.Map as Map import Data.Map ((!), (!?)) import qualified Data.HashSet as HashSet instance IsInvitableJunction ExamOfficeUser where type InvitationFor ExamOfficeUser = User data InvitableJunction ExamOfficeUser = JunctionExamOfficeUser deriving (Eq, Ord, Read, Show, Generic, Typeable) data InvitationDBData ExamOfficeUser = InvDBDataExamOfficeUser deriving (Eq, Ord, Read, Show, Generic, Typeable) data InvitationTokenData ExamOfficeUser = InvTokenDataExamOfficeUser { invTokenExamOfficeUserOffice :: CryptoUUIDUser } deriving (Eq, Ord, Read, Show, Generic, Typeable) _InvitableJunction = iso (\ExamOfficeUser{..} -> (examOfficeUserUser, examOfficeUserOffice, JunctionExamOfficeUser)) (\(examOfficeUserUser, examOfficeUserOffice, JunctionExamOfficeUser) -> ExamOfficeUser{..}) instance ToJSON (InvitableJunction ExamOfficeUser) where toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 } toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 1 } instance FromJSON (InvitableJunction ExamOfficeUser) where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 } instance ToJSON (InvitationDBData ExamOfficeUser) where toJSON = genericToJSON defaultOptions { constructorTagModifier = camelToPathPiece' 3 } toEncoding = genericToEncoding defaultOptions { constructorTagModifier = camelToPathPiece' 3 } instance FromJSON (InvitationDBData ExamOfficeUser) where parseJSON = genericParseJSON defaultOptions { constructorTagModifier = camelToPathPiece' 3 } instance ToJSON (InvitationTokenData ExamOfficeUser) where toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 5 } toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 5 } instance FromJSON (InvitationTokenData ExamOfficeUser) where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 5 } examOfficeUserInvitationConfig :: InvitationConfig ExamOfficeUser examOfficeUserInvitationConfig = InvitationConfig{..} where invitationRoute _ _ = return $ ExamOfficeR EOUsersInviteR invitationResolveFor InvTokenDataExamOfficeUser{..} = do officeId <- decrypt invTokenExamOfficeUserOffice bool notFound (return officeId) =<< existsKey officeId invitationSubject (Entity _ User{..}) _ = do return . SomeMessage $ MsgMailSubjectExamOfficeUserInvitation userDisplayName invitationHeading (Entity _ User{..}) _ = do return . SomeMessage $ MsgExamOfficeUserInviteHeading userDisplayName invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamOfficeUserInviteExplanation}|] invitationTokenConfig _ _ = do itAuthority <- HashSet.singleton . Right <$> liftHandler requireAuthId let itExpiresAt = Nothing itStartsAt = Nothing itAddAuth = Nothing return InvitationTokenConfig{..} invitationRestriction _ _ = return Authorized invitationForm _ _ _ = pure (JunctionExamOfficeUser, ()) invitationInsertHook _ _ _ ExamOfficeUser{..} _ act = do res <- act audit $ TransactionExamOfficeUserAdd examOfficeUserOffice examOfficeUserUser return res invitationSuccessMsg _ _ = return $ SomeMessage MsgExamOfficeUserInvitationAccepted invitationUltDest _ _ = return $ SomeRoute NewsR makeExamOfficeUsersForm :: Maybe (Set (Either UserEmail UserId)) -> Form (Set (Either UserEmail UserId)) makeExamOfficeUsersForm template = renderWForm FormStandard $ do cRoute <- fromMaybe (error "makeExamOfficeUsersForm called from 404-handler") <$> getCurrentRoute let miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId]) miAdd' nudge btn csrf = do MsgRenderer mr <- getMsgRenderer (addRes, addView) <- mpreq (multiUserInvitationField $ MUILookupAnyUser Nothing) (fslI MsgExamOfficeUserEmail & addName (nudge "users") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing let res' :: FormResult ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId]) res' = addRes <&> \newUsers oldUsers -> if | null newUsers -> pure oldUsers | otherwise -> pure . nub $ oldUsers ++ Set.toList newUsers return (res', $(widgetFile "widgets/massinput/examOfficeUsers/add")) miCell' :: Either UserEmail UserId -> Widget miCell' (Left email) = do invWarnMsg <- messageIconI Info IconEmail MsgEmailInvitationWarning $(widgetFile "widgets/massinput/examOfficeUsers/cellInvitation") miCell' (Right uid) = do User{..} <- liftHandler . runDB $ getJust uid $(widgetFile "widgets/massinput/examOfficeUsers/cellKnown") miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction' frag = Just . SomeRoute $ cRoute :#: frag miLayout' :: MassInputLayout ListLength _ () miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/examOfficeUsers/layout") miIdent' :: Text miIdent' = "exam-office-users" fSettings :: FieldSettings UniWorX fSettings = fslI MsgExamOfficeSubscribedUsers & setTooltip MsgExamOfficeSubscribedUsersTip fRequired :: Bool fRequired = False template' <- for template $ \uids -> liftHandler . runDB $ do let (invitations, knownUsers) = partitionEithers $ Set.toList uids knownUsers' <- fmap (map E.unValue) . E.select . E.from $ \user -> do E.where_ $ user E.^. UserId `E.in_` E.valList knownUsers E.orderBy [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName] return $ user E.^. UserId return $ map Left invitations ++ map Right knownUsers' fmap Set.fromList <$> massInputAccumW miAdd' miCell' miButtonAction' miLayout' miIdent' fSettings fRequired template' -- | Manage the list of users this user (in her function as exam-office) -- has an interest in, i.e. that authorize her to view their grades getEOUsersR, postEOUsersR :: Handler Html getEOUsersR = postEOUsersR postEOUsersR = do uid <- requireAuthId oldUsers <- liftHandler . runDB $ do users <- E.select . E.from $ \(user `E.InnerJoin` examOfficeUser) -> do E.on $ user E.^. UserId E.==. examOfficeUser E.^. ExamOfficeUserUser E.&&. examOfficeUser E.^. ExamOfficeUserOffice E.==. E.val uid return $ user E.^. UserId invites <- Map.keysSet <$> sourceInvitationsF @ExamOfficeUser uid return $ setOf (folded . _Value . re _Right) users <> Set.mapMonotonic Left invites ((usersRes, usersView), usersEnc) <- runFormPost . makeExamOfficeUsersForm $ Just oldUsers formResult usersRes $ \(setSymmDiff oldUsers -> changes) -> do liftHandler . runDBJobs . forM_ changes $ \change -> if | change `Set.member` oldUsers -> case change of Right change' -> do deleteBy $ UniqueExamOfficeUser uid change' audit $ TransactionExamOfficeUserDelete uid change' Left change' -> deleteInvitation @ExamOfficeUser uid change' | otherwise -> case change of Right change' -> do insert_ $ ExamOfficeUser uid change' audit $ TransactionExamOfficeUserAdd uid change' Left change' -> do cID <- encrypt uid sinkInvitation examOfficeUserInvitationConfig (change', uid, (InvDBDataExamOfficeUser, InvTokenDataExamOfficeUser cID)) addMessageI Success $ MsgTransactionExamOfficeUsersUpdated (Set.size $ changes `Set.intersection` oldUsers) (Set.size $ changes `Set.difference` oldUsers) redirect $ ExamOfficeR EOExamsR let usersView' = wrapForm usersView def { formAction = Just . SomeRoute $ ExamOfficeR EOUsersR , formEncoding = usersEnc } siteLayoutMsg MsgMenuExamOfficeUsers $ do setTitleI MsgMenuExamOfficeUsers [whamlet| $newline never

_{MsgExamOfficeSubscribedUsersExplanation} ^{usersView'} |] getEOUsersInviteR, postEOUsersInviteR :: Handler Html getEOUsersInviteR = postEOUsersInviteR postEOUsersInviteR = invitationR examOfficeUserInvitationConfig