-- SPDX-FileCopyrightText: 2022 Sarah Vaupel -- -- SPDX-License-Identifier: AGPL-3.0-or-later module Handler.Course.ParticipantInvite ( getCAddUserR, postCAddUserR ) where import Import import Handler.Utils import Handler.Utils.Avs import Jobs.Queue import qualified Data.Aeson as Aeson import qualified Data.CaseInsensitive as CI import Data.Map ((!)) import qualified Data.Map as Map import qualified Data.Text as Text import qualified Data.Time.Zones as TZ import qualified Data.Set as Set import Control.Monad.Except (MonadError(..)) import Generics.Deriving.Monoid (memptydefault, mappenddefault) type UserSearchKey = Text type TutorialIdent = CI Text data ButtonCourseRegisterMode = BtnCourseRegisterConfirm | BtnCourseRegisterAbort deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) instance Universe ButtonCourseRegisterMode instance Finite ButtonCourseRegisterMode embedRenderMessage ''UniWorX ''ButtonCourseRegisterMode id nullaryPathPiece ''ButtonCourseRegisterMode $ camelToPathPiece' 1 instance Button UniWorX ButtonCourseRegisterMode where btnLabel x = [whamlet|_{x}|] btnClasses BtnCourseRegisterConfirm = [BCIsButton, BCPrimary] btnClasses BtnCourseRegisterAbort = [BCIsButton, BCDanger] btnValidate _ BtnCourseRegisterAbort = False btnValidate _ _ = True data CourseRegisterAction = CourseRegisterActionAddParticipant | CourseRegisterActionAddTutorialMember -- | CourseRegisterActionUnknownPerson deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) instance Universe CourseRegisterAction instance Finite CourseRegisterAction data CourseRegisterActionData = CourseRegisterActionAddParticipantData { crActAddParticipantIdent :: UserSearchKey , crActAddParticipantUser :: (UserId, User) } | CourseRegisterActionAddTutorialMemberData { crActAddTutorialMemberIdent :: UserSearchKey , crActAddTutorialMemberUser :: (UserId, User) , crActAddTutorialMemberTutorial :: TutorialIdent } -- | CourseRegisterActionUnknownPersonData -- pseudo-action; just for display -- { crActUnknownPersonIdent :: Text -- } deriving (Eq, Ord, Show, Generic, Typeable) makeLenses_ ''CourseRegisterActionData instance Aeson.FromJSON CourseRegisterActionData where parseJSON = Aeson.genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 2 } instance Aeson.ToJSON CourseRegisterActionData where toJSON = Aeson.genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 2 } toEncoding = Aeson.genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 2 } _classifyRegisterAction :: CourseRegisterActionData -> CourseRegisterAction _classifyRegisterAction = \case CourseRegisterActionAddParticipantData{} -> CourseRegisterActionAddParticipant CourseRegisterActionAddTutorialMemberData{} -> CourseRegisterActionAddTutorialMember --CourseRegisterActionUnknownPersonData{} -> CourseRegisterActionUnknownPerson courseRegisterRenderActionClass :: CourseRegisterAction -> Widget courseRegisterRenderActionClass = \case CourseRegisterActionAddParticipant -> [whamlet|_{MsgCourseParticipantsRegisterActionAddParticipants}|] CourseRegisterActionAddTutorialMember -> [whamlet|_{MsgCourseParticipantsRegisterActionAddTutorialMembers}|] courseRegisterRenderAction :: CourseRegisterActionData -> Widget courseRegisterRenderAction = \case CourseRegisterActionAddParticipantData{..} -> [whamlet|^{userWidget (view _2 crActAddParticipantUser)} (#{crActAddParticipantIdent})|] CourseRegisterActionAddTutorialMemberData{..} -> [whamlet|^{userWidget (view _2 crActAddTutorialMemberUser)} (#{crActAddTutorialMemberIdent}), _{MsgCourseParticipantsRegisterTutorialField}: #{crActAddTutorialMemberTutorial}|] data AddUserRequest = AddUserRequest { auReqUsers :: Set UserSearchKey , auReqTutorial :: Maybe TutorialIdent } deriving (Eq, Ord, Read, Show, Generic, Typeable) data AddParticipantsResult = AddParticipantsResult { aurNotFound :: Set UserSearchKey , aurAlreadyRegistered , aurAlreadyTutorialMember , aurRegisterSuccess , aurTutorialSuccess :: Set UserId } deriving (Read, Show, Generic, Typeable) instance Semigroup AddParticipantsResult where (<>) = mappenddefault instance Monoid AddParticipantsResult where mempty = memptydefault mappend = (<>) getCAddUserR, postCAddUserR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCAddUserR = postCAddUserR postCAddUserR tid ssh csh = do cid <- runDB . getKeyBy404 $ TermSchoolCourseShort tid ssh csh currentRoute <- fromMaybe (error "postCAddUserR called from 404-handler") <$> getCurrentRoute confirmAvailableActs <- fmap (maybe FormMissing FormSuccess) . throwExceptT . runMaybeT $ encodedSecretBoxOpen =<< MaybeT (lift . lookupPostParam $ toPathPiece PostCourseUserAddConfirmAvailableActions) confirmActs <- fmap (maybe FormMissing FormSuccess) . throwExceptT . runMaybeT $ encodedSecretBoxOpen =<< MaybeT (lift . lookupPostParam $ toPathPiece PostCourseUserAddConfirmAction) $logErrorS "CAddUserR available" . tshow $ Aeson.encode (confirmAvailableActs :: Maybe (Set CourseRegisterActionData)) $logErrorS "CAddUserR acts" . tshow $ Aeson.encode (confirmActs :: Maybe (Set CourseRegisterActionData)) if | FormSuccess acts <- confirmActs , FormSuccess _availableActs <- confirmAvailableActs -> do -- TODO: check that all acts are member of availableActs! forM_ (Set.toList acts) $ \case CourseRegisterActionAddTutorialMemberData{..} -> do registeredUsers <- registerUsers cid $ Map.singleton crActAddTutorialMemberIdent (Just $ view _1 crActAddTutorialMemberUser) tutId <- upsertNewTutorial cid crActAddTutorialMemberTutorial registerTutorialMembers tutId registeredUsers redirect $ CTutorialR tid ssh csh crActAddTutorialMemberTutorial TUsersR CourseRegisterActionAddParticipantData{..} -> do void . registerUsers cid $ Map.singleton crActAddParticipantIdent (Just $ view _1 crActAddParticipantUser) redirect $ CourseR tid ssh csh CUsersR | FormSuccess _ <- confirmActsRes -> addMessageI Error MsgCourseParticipantsRegisterConfirmInvalid | FormMissing <- confirmActsRes -> return () | FormFailure errs <- confirmActsRes -> forM_ errs $ addMessage Error . toHtml ((usersToAdd :: FormResult AddUserRequest, formWgt), formEncoding) <- runFormPost . renderWForm FormStandard $ do today <- localDay . TZ.utcToLocalTimeTZ appTZ <$> liftIO getCurrentTime let cfCommaSeparatedSet :: forall m. Functor m => Field m Text -> Field m (Set Text) cfCommaSeparatedSet = guardField (not . Set.null) . convertField (Set.fromList . mapMaybe (assertM' (not . Text.null) . Text.strip) . Text.splitOn ",") (Text.intercalate ", " . Set.toList) auReqUsers <- wreq (textField & cfCommaSeparatedSet) (fslI MsgCourseParticipantsRegisterUsersField & setTooltip MsgCourseParticipantsRegisterUsersFieldTip) mempty auReqTutorial <- optionalActionW ( areq (textField & cfCI) (fslI MsgCourseParticipantsRegisterTutorialField & setTooltip MsgCourseParticipantsRegisterTutorialFieldTip) (Just . CI.mk $ tshow today) ) -- TODO: use user date display setting ( fslI MsgCourseParticipantsRegisterTutorialOption ) ( Just True ) return $ AddUserRequest <$> auReqUsers <*> auReqTutorial formResult usersToAdd $ \AddUserRequest{..} -> do avsUsers :: Map UserSearchKey (Maybe UserId) <- sequenceA $ Map.fromSet upsertAvsUser auReqUsers let (usersFound, usersNotFound) = partition (is _Just . view _2) $ Map.toList avsUsers unless (null usersNotFound) $ let msgContent = [whamlet| $newline never