{-# OPTIONS_GHC -fno-warn-orphans #-} module Handler.Exam where import Import import Handler.Utils import Handler.Utils.Exam import Handler.Utils.Invitations import Handler.Utils.Table.Columns import Handler.Utils.Table.Cells import Handler.Utils.Csv import Jobs.Queue import Utils.Lens hiding (parts) import qualified Database.Esqueleto as E import qualified Database.Esqueleto.Utils as E import Database.Esqueleto.Utils.TH import Data.Map ((!), (!?)) import qualified Data.Map as Map import qualified Data.Set as Set import qualified Data.Text as Text import qualified Data.Text.Lens as Text import Data.Aeson hiding (Result(..)) import Text.Hamlet (ihamlet) import Text.Blaze.Html.Renderer.String (renderHtml) import qualified Data.CaseInsensitive as CI import qualified Control.Monad.State.Class as State import Control.Arrow (Kleisli(..)) import qualified Data.Csv as Csv import qualified Data.Conduit.List as C import Numeric.Lens (integral) import Database.Persist.Sql (deleteWhereCount, updateWhereCount) -- Dedicated ExamRegistrationButton data ButtonExamRegister = BtnExamRegister | BtnExamDeregister deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) instance Universe ButtonExamRegister instance Finite ButtonExamRegister nullaryPathPiece ''ButtonExamRegister $ camelToPathPiece' 1 embedRenderMessage ''UniWorX ''ButtonExamRegister id instance Button UniWorX ButtonExamRegister where btnClasses BtnExamRegister = [BCIsButton, BCPrimary] btnClasses BtnExamDeregister = [BCIsButton, BCDanger] btnLabel BtnExamRegister = [whamlet|#{iconExamRegister True} _{MsgBtnExamRegister}|] btnLabel BtnExamDeregister = [whamlet|#{iconExamRegister False} _{MsgBtnExamDeregister}|] getCExamListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCExamListR tid ssh csh = do Entity cid Course{..} <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh now <- liftIO getCurrentTime mayCreate <- hasWriteAccessTo $ CourseR tid ssh csh CExamNewR let examDBTable = DBTable{..} where dbtSQLQuery exam = do E.where_ $ exam E.^. ExamCourse E.==. E.val cid return exam dbtRowKey = (E.^. ExamId) dbtProj x@DBRow{ dbrOutput = Entity _ Exam{..} } = do guardM . hasReadAccessTo $ CExamR tid ssh csh examName EShowR return x dbtColonnade = dbColonnade . mconcat $ catMaybes [ Just . sortable (Just "name") (i18nCell MsgExamName) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> anchorCell (CExamR tid ssh csh examName EShowR) examName , (<$ guard mayCreate) . sortable (Just "visible") (i18nCell MsgExamVisibleFrom) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty (dateTimeCellVisible now) examVisibleFrom , Just . sortable (Just "register-from") (i18nCell MsgExamRegisterFrom) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterFrom , Just . sortable (Just "register-to") (i18nCell MsgExamRegisterTo) $ \DBRow { dbrOutput = Entity _ Exam{..} } -> maybe mempty dateTimeCell examRegisterTo , Just . sortable (Just "time") (i18nCell MsgExamTime) $ \DBRow{ dbrOutput = Entity _ Exam{..} } -> maybe mempty (cell . flip (formatTimeRangeW SelFormatDateTime) examEnd) examStart ] dbtSorting = Map.fromList [ ("name", SortColumn $ \exam -> exam E.^. ExamName ) , ("time", SortColumn $ \exam -> exam E.^. ExamStart ) , ("register-from", SortColumn $ \exam -> exam E.^. ExamRegisterFrom ) , ("register-to", SortColumn $ \exam -> exam E.^. ExamRegisterTo ) , ("visible", SortColumn $ \exam -> exam E.^. ExamVisibleFrom ) ] dbtFilter = Map.empty dbtFilterUI = const mempty dbtStyle = def dbtParams = def dbtIdent :: Text dbtIdent = "exams" dbtCsvEncode = noCsvEncode dbtCsvDecode = Nothing examDBTableValidator = def & defaultSorting [SortAscBy "time"] ((), examTable) <- runDB $ dbTable examDBTableValidator examDBTable siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamsHeading) $ do setTitleI $ prependCourseTitle tid ssh csh MsgExamsHeading $(widgetFile "exam-list") instance IsInvitableJunction ExamCorrector where type InvitationFor ExamCorrector = Exam data InvitableJunction ExamCorrector = JunctionExamCorrector deriving (Eq, Ord, Read, Show, Generic, Typeable) data InvitationDBData ExamCorrector = InvDBDataExamCorrector deriving (Eq, Ord, Read, Show, Generic, Typeable) data InvitationTokenData ExamCorrector = InvTokenDataExamCorrector deriving (Eq, Ord, Read, Show, Generic, Typeable) _InvitableJunction = iso (\ExamCorrector{..} -> (examCorrectorUser, examCorrectorExam, JunctionExamCorrector)) (\(examCorrectorUser, examCorrectorExam, JunctionExamCorrector) -> ExamCorrector{..}) instance ToJSON (InvitableJunction ExamCorrector) where toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 } toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 1 } instance FromJSON (InvitableJunction ExamCorrector) where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 } instance ToJSON (InvitationDBData ExamCorrector) where toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 4 } toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 4 } instance FromJSON (InvitationDBData ExamCorrector) where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 4 } instance ToJSON (InvitationTokenData ExamCorrector) where toJSON = genericToJSON defaultOptions { constructorTagModifier = camelToPathPiece' 4 } toEncoding = genericToEncoding defaultOptions { constructorTagModifier = camelToPathPiece' 4 } instance FromJSON (InvitationTokenData ExamCorrector) where parseJSON = genericParseJSON defaultOptions { constructorTagModifier = camelToPathPiece' 4 } examCorrectorInvitationConfig :: InvitationConfig ExamCorrector examCorrectorInvitationConfig = InvitationConfig{..} where invitationRoute (Entity _ Exam{..}) _ = do Course{..} <- get404 examCourse return $ CExamR courseTerm courseSchool courseShorthand examName ECInviteR invitationResolveFor = do Just (CExamR tid csh ssh examn ECInviteR) <- getCurrentRoute fetchExamId tid csh ssh examn invitationSubject Exam{..} _ = do Course{..} <- get404 examCourse return . SomeMessage $ MsgMailSubjectExamCorrectorInvitation courseTerm courseSchool courseShorthand examName invitationHeading Exam{..} _ = return . SomeMessage $ MsgExamCorrectorInviteHeading examName invitationExplanation _ _ = [ihamlet|_{SomeMessage MsgExamCorrectorInviteExplanation}|] invitationTokenConfig _ _ = do itAuthority <- liftHandlerT requireAuthId return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing invitationRestriction _ _ = return Authorized invitationForm _ _ _ = pure JunctionExamCorrector invitationSuccessMsg Exam{..} _ = return . SomeMessage $ MsgExamCorrectorInvitationAccepted examName invitationUltDest Exam{..} _ = do Course{..} <- get404 examCourse return . SomeRoute $ CourseR courseTerm courseSchool courseShorthand CExamListR getECInviteR, postECInviteR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getECInviteR = postECInviteR postECInviteR = invitationR examCorrectorInvitationConfig data ExamForm = ExamForm { efName :: ExamName , efDescription :: Maybe Html , efStart :: Maybe UTCTime , efEnd :: Maybe UTCTime , efVisibleFrom :: Maybe UTCTime , efRegisterFrom :: Maybe UTCTime , efRegisterTo :: Maybe UTCTime , efDeregisterUntil :: Maybe UTCTime , efPublishOccurrenceAssignments :: Maybe UTCTime , efFinished :: Maybe UTCTime , efClosed :: Maybe UTCTime , efOccurrences :: Set ExamOccurrenceForm , efShowGrades :: Bool , efPublicStatistics :: Bool , efGradingRule :: ExamGradingRule , efBonusRule :: ExamBonusRule , efOccurrenceRule :: ExamOccurrenceRule , efCorrectors :: Set (Either UserEmail UserId) , efExamParts :: Set ExamPartForm } data ExamOccurrenceForm = ExamOccurrenceForm { eofId :: Maybe CryptoUUIDExamOccurrence , eofName :: ExamOccurrenceName , eofRoom :: Text , eofCapacity :: Natural , eofStart :: UTCTime , eofEnd :: Maybe UTCTime , eofDescription :: Maybe Html } deriving (Read, Show, Eq, Ord, Generic, Typeable) data ExamPartForm = ExamPartForm { epfId :: Maybe CryptoUUIDExamPart , epfName :: ExamPartName , epfMaxPoints :: Maybe Points , epfWeight :: Rational } deriving (Read, Show, Eq, Ord, Generic, Typeable) makeLenses_ ''ExamForm deriveJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 } ''ExamPartForm deriveJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 } ''ExamOccurrenceForm examForm :: Maybe ExamForm -> Form ExamForm examForm template html = do MsgRenderer mr <- getMsgRenderer flip (renderAForm FormStandard) html $ ExamForm <$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template) <*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslpI MsgExamDescription "Html") (efDescription <$> template)) <* aformSection MsgExamFormTimes <*> aopt utcTimeField (fslpI MsgExamStart (mr MsgDate) & setTooltip MsgExamTimeTip) (efStart <$> template) <*> aopt utcTimeField (fslpI MsgExamEnd (mr MsgDate) & setTooltip MsgExamTimeTip) (efEnd <$> template) <*> aopt utcTimeField (fslpI MsgExamVisibleFrom (mr MsgDate) & setTooltip MsgExamVisibleFromTip) (efVisibleFrom <$> template) <*> aopt utcTimeField (fslpI MsgExamRegisterFrom (mr MsgDate) & setTooltip MsgExamRegisterFromTip) (efRegisterFrom <$> template) <*> aopt utcTimeField (fslpI MsgExamRegisterTo (mr MsgDate)) (efRegisterTo <$> template) <*> aopt utcTimeField (fslpI MsgExamDeregisterUntil (mr MsgDate)) (efDeregisterUntil <$> template) <*> aopt utcTimeField (fslpI MsgExamPublishOccurrenceAssignments (mr MsgDate) & setTooltip MsgExamPublishOccurrenceAssignmentsTip) (efPublishOccurrenceAssignments <$> template) <*> aopt utcTimeField (fslpI MsgExamFinished (mr MsgDate) & setTooltip MsgExamFinishedTip) (efFinished <$> template) <*> aopt utcTimeField (fslpI MsgExamClosed (mr MsgDate) & setTooltip MsgExamClosedTip) (efClosed <$> template) <* aformSection MsgExamFormOccurrences <*> examOccurrenceForm (efOccurrences <$> template) <* aformSection MsgExamFormAutomaticFunctions <*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamShowGrades & setTooltip MsgExamShowGradesTip) (Just . efShowGrades <$> template)) <*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamPublicStatistics & setTooltip MsgExamPublicStatisticsTip) (Just . efPublicStatistics <$> template)) <*> examGradingRuleForm (efGradingRule <$> template) <*> examBonusRuleForm (efBonusRule <$> template) <*> examOccurrenceRuleForm (efOccurrenceRule <$> template) <* aformSection MsgExamFormCorrection <*> examCorrectorsForm (efCorrectors <$> template) <* aformSection MsgExamFormParts <*> examPartsForm (efExamParts <$> template) examCorrectorsForm :: Maybe (Set (Either UserEmail UserId)) -> AForm Handler (Set (Either UserEmail UserId)) examCorrectorsForm mPrev = wFormToAForm $ do MsgRenderer mr <- getMsgRenderer Just currentRoute <- getCurrentRoute uid <- liftHandlerT requireAuthId let miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId]) miAdd' nudge submitView csrf = do (addRes, addView) <- mpreq (multiUserField False $ Just corrUserSuggestions) ("" & addName (nudge "email")) Nothing let addRes' | otherwise = addRes <&> \newDat oldDat -> if | existing <- newDat `Set.intersection` Set.fromList oldDat , not $ Set.null existing -> FormFailure [mr MsgExamCorrectorAlreadyAdded] | otherwise -> FormSuccess $ Set.toList newDat return (addRes', $(widgetFile "widgets/massinput/examCorrectors/add")) corrUserSuggestions :: E.SqlQuery (E.SqlExpr (Entity User)) corrUserSuggestions = E.from $ \(lecturer `E.InnerJoin` course `E.InnerJoin` exam `E.InnerJoin` corrector `E.InnerJoin` corrUser) -> do E.on $ corrUser E.^. UserId E.==. corrector E.^. ExamCorrectorUser E.on $ corrector E.^. ExamCorrectorExam E.==. exam E.^. ExamId E.on $ exam E.^. ExamCourse E.==. course E.^. CourseId E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid return corrUser miCell' :: Either UserEmail UserId -> Widget miCell' (Left email) = $(widgetFile "widgets/massinput/examCorrectors/cellInvitation") miCell' (Right userId) = do User{..} <- liftHandlerT . runDB $ get404 userId $(widgetFile "widgets/massinput/examCorrectors/cellKnown") miLayout' :: MassInputLayout ListLength (Either UserEmail UserId) () miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/examCorrectors/layout") fmap Set.fromList <$> massInputAccumW miAdd' miCell' miButtonAction' miLayout' ("correctors" :: Text) (fslI MsgExamCorrectors & setTooltip MsgMassInputTip) False (Set.toList <$> mPrev) examOccurrenceForm :: Maybe (Set ExamOccurrenceForm) -> AForm Handler (Set ExamOccurrenceForm) examOccurrenceForm prev = wFormToAForm $ do Just currentRoute <- getCurrentRoute let miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag fmap (fmap Set.fromList) . massInputAccumEditW miAdd' miCell' miButtonAction' miLayout' miIdent' (fslI MsgExamOccurrences & setTooltip MsgMassInputTip) False $ Set.toList <$> prev where examOccurrenceForm' nudge mPrev csrf = do (eofIdRes, eofIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ eofId =<< mPrev) (eofNameRes, eofNameView) <- mpreq ciField ("" & addName (nudge "name")) (eofName <$> mPrev) (eofRoomRes, eofRoomView) <- mpreq textField ("" & addName (nudge "room")) (eofRoom <$> mPrev) (eofCapacityRes, eofCapacityView) <- mpreq (natFieldI MsgExamRoomCapacityNegative) ("" & addName (nudge "capacity")) (eofCapacity <$> mPrev) (eofStartRes, eofStartView) <- mpreq utcTimeField ("" & addName (nudge "start")) (eofStart <$> mPrev) (eofEndRes, eofEndView) <- mopt utcTimeField ("" & addName (nudge "end")) (eofEnd <$> mPrev) (eofDescRes, eofDescView) <- mopt htmlFieldSmall ("" & addName (nudge "description")) (eofDescription <$> mPrev) return ( ExamOccurrenceForm <$> eofIdRes <*> eofNameRes <*> eofRoomRes <*> eofCapacityRes <*> eofStartRes <*> eofEndRes <*> (assertM (not . null . renderHtml) <$> eofDescRes) , $(widgetFile "widgets/massinput/examRooms/form") ) miAdd' nudge submitView csrf = do MsgRenderer mr <- getMsgRenderer (res, formWidget) <- examOccurrenceForm' nudge Nothing csrf let addRes = res <&> \newDat (Set.fromList -> oldDat) -> if | newDat `Set.member` oldDat -> FormFailure [mr MsgExamRoomAlreadyExists] | otherwise -> FormSuccess $ pure newDat return (addRes, $(widgetFile "widgets/massinput/examRooms/add")) miCell' nudge dat = examOccurrenceForm' nudge (Just dat) miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/examRooms/layout") miIdent' :: Text miIdent' = "exam-occurrences" examPartsForm :: Maybe (Set ExamPartForm) -> AForm Handler (Set ExamPartForm) examPartsForm prev = wFormToAForm $ do Just currentRoute <- getCurrentRoute let miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag fmap (fmap Set.fromList) . massInputAccumEditW miAdd' miCell' miButtonAction' miLayout' miIdent' (fslI MsgExamParts & setTooltip MsgMassInputTip) False $ Set.toList <$> prev where examPartForm' nudge mPrev csrf = do (epfIdRes, epfIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ epfId =<< mPrev) (epfNameRes, epfNameView) <- mpreq ciField ("" & addName (nudge "name")) (epfName <$> mPrev) (epfMaxPointsRes, epfMaxPointsView) <- mopt pointsField ("" & addName (nudge "max-points")) (epfMaxPoints <$> mPrev) (epfWeightRes, epfWeightView) <- mpreq (checkBool (>= 0) MsgExamPartWeightNegative rationalField) ("" & addName (nudge "weight")) (epfWeight <$> mPrev <|> Just 1) return ( ExamPartForm <$> epfIdRes <*> epfNameRes <*> epfMaxPointsRes <*> epfWeightRes , $(widgetFile "widgets/massinput/examParts/form") ) miAdd' nudge submitView csrf = do MsgRenderer mr <- getMsgRenderer (res, formWidget) <- examPartForm' nudge Nothing csrf let addRes = res <&> \newDat (Set.fromList -> oldDat) -> if | any (((==) `on` epfName) newDat) oldDat -> FormFailure [mr MsgExamPartAlreadyExists] | otherwise -> FormSuccess $ pure newDat return (addRes, $(widgetFile "widgets/massinput/examParts/add")) miCell' nudge dat = examPartForm' nudge (Just dat) miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/examParts/layout") miIdent' :: Text miIdent' = "exam-parts" examFormTemplate :: Entity Exam -> DB ExamForm examFormTemplate (Entity eId Exam{..}) = do parts <- selectList [ ExamPartExam ==. eId ] [] occurrences <- selectList [ ExamOccurrenceExam ==. eId ] [] correctors <- selectList [ ExamCorrectorExam ==. eId ] [] invitations <- map (\(email, InvDBDataExamCorrector) -> email) <$> sourceInvitationsList eId parts' <- forM parts $ \(Entity pid part) -> (,) <$> encrypt pid <*> pure part occurrences' <- forM occurrences $ \(Entity oid occ) -> (,) <$> encrypt oid <*> pure occ return ExamForm { efName = examName , efGradingRule = examGradingRule , efBonusRule = examBonusRule , efOccurrenceRule = examOccurrenceRule , efVisibleFrom = examVisibleFrom , efRegisterFrom = examRegisterFrom , efRegisterTo = examRegisterTo , efDeregisterUntil = examDeregisterUntil , efPublishOccurrenceAssignments = examPublishOccurrenceAssignments , efStart = examStart , efEnd = examEnd , efFinished = examFinished , efClosed = examClosed , efShowGrades = examShowGrades , efPublicStatistics = examPublicStatistics , efDescription = examDescription , efOccurrences = Set.fromList $ do (Just -> eofId, ExamOccurrence{..}) <- occurrences' return ExamOccurrenceForm { eofId , eofName = examOccurrenceName , eofRoom = examOccurrenceRoom , eofCapacity = examOccurrenceCapacity , eofStart = examOccurrenceStart , eofEnd = examOccurrenceEnd , eofDescription = examOccurrenceDescription } , efExamParts = Set.fromList $ do (Just -> epfId, ExamPart{..}) <- parts' return ExamPartForm { epfId , epfName = examPartName , epfMaxPoints = examPartMaxPoints , epfWeight = examPartWeight } , efCorrectors = Set.unions [ Set.fromList $ map Left invitations , Set.fromList . map Right $ do Entity _ ExamCorrector{..} <- correctors return examCorrectorUser ] } examTemplate :: CourseId -> DB (Maybe ExamForm) examTemplate cid = runMaybeT $ do newCourse <- MaybeT $ get cid [(Entity _ oldCourse, Entity _ oldExam)] <- lift . E.select . E.from $ \(course `E.InnerJoin` exam) -> do E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse E.where_ $ ( course E.^. CourseShorthand E.==. E.val (courseShorthand newCourse) E.||. course E.^. CourseName E.==. E.val (courseName newCourse) ) E.&&. course E.^. CourseSchool E.==. E.val (courseSchool newCourse) E.where_ . E.not_ . E.exists . E.from $ \exam' -> do E.where_ $ exam' E.^. ExamCourse E.==. E.val cid E.where_ $ exam E.^. ExamName E.==. exam' E.^. ExamName E.where_ . E.not_ . E.isNothing $ exam E.^. ExamVisibleFrom E.limit 1 E.orderBy [ E.desc $ course E.^. CourseTerm, E.asc $ exam E.^. ExamVisibleFrom ] return (course, exam) oldTerm <- MaybeT . get $ courseTerm oldCourse newTerm <- MaybeT . get $ courseTerm newCourse let dateOffset = over _utctDay . addDays $ (diffDays `on` termLectureEnd) newTerm oldTerm return ExamForm { efName = examName oldExam , efGradingRule = examGradingRule oldExam , efBonusRule = examBonusRule oldExam , efOccurrenceRule = examOccurrenceRule oldExam , efVisibleFrom = dateOffset <$> examVisibleFrom oldExam , efRegisterFrom = dateOffset <$> examRegisterFrom oldExam , efRegisterTo = dateOffset <$> examRegisterTo oldExam , efDeregisterUntil = dateOffset <$> examDeregisterUntil oldExam , efPublishOccurrenceAssignments = dateOffset <$> examPublishOccurrenceAssignments oldExam , efStart = dateOffset <$> examStart oldExam , efEnd = dateOffset <$> examEnd oldExam , efFinished = dateOffset <$> examFinished oldExam , efClosed = dateOffset <$> examClosed oldExam , efShowGrades = examShowGrades oldExam , efPublicStatistics = examPublicStatistics oldExam , efDescription = examDescription oldExam , efOccurrences = Set.empty , efExamParts = Set.empty , efCorrectors = Set.empty } validateExam :: (MonadHandler m, HandlerSite m ~ UniWorX) => FormValidator ExamForm m () validateExam = do ExamForm{..} <- State.get guardValidation MsgExamRegisterToMustBeAfterRegisterFrom $ NTop efRegisterTo >= NTop efRegisterFrom guardValidation MsgExamDeregisterUntilMustBeAfterRegisterFrom $ NTop efDeregisterUntil >= NTop efRegisterFrom guardValidation MsgExamStartMustBeAfterPublishOccurrenceAssignments . fromMaybe True $ (>=) <$> efStart <*> efPublishOccurrenceAssignments guardValidation MsgExamEndMustBeAfterStart $ NTop efEnd >= NTop efStart guardValidation MsgExamFinishedMustBeAfterEnd . fromMaybe True $ (>=) <$> efFinished <*> efEnd guardValidation MsgExamFinishedMustBeAfterStart $ NTop efFinished >= NTop efStart guardValidation MsgExamClosedMustBeAfterFinished . fromMaybe True $ (>=) <$> efClosed <*> efFinished guardValidation MsgExamClosedMustBeAfterStart $ NTop efClosed >= NTop efStart guardValidation MsgExamClosedMustBeAfterEnd . fromMaybe True $ (>=) <$> efClosed <*> efEnd forM_ efOccurrences $ \ExamOccurrenceForm{..} -> do guardValidation (MsgExamOccurrenceEndMustBeAfterStart eofName) $ NTop eofEnd >= NTop (Just eofStart) guardValidation (MsgExamOccurrenceStartMustBeAfterExamStart eofName) $ NTop (Just eofStart) >= NTop efStart guardValidation (MsgExamOccurrenceEndMustBeBeforeExamEnd eofName) $ NTop eofEnd <= NTop efEnd forM_ [ (a, b) | a <- Set.toAscList efOccurrences, b <- Set.toAscList efOccurrences, b > a ] $ \(a, b) -> do eofRange' <- formatTimeRange SelFormatDateTime (eofStart a) (eofEnd a) guardValidation (MsgExamOccurrenceDuplicate (eofRoom a) eofRange') $ any (\f -> f a b) [ (/=) `on` eofRoom , (/=) `on` eofStart , (/=) `on` eofEnd , (/=) `on` fmap renderHtml . eofDescription ] guardValidation (MsgExamOccurrenceDuplicateName $ eofName a) $ ((/=) `on` eofName) a b getCExamNewR, postCExamNewR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCExamNewR = postCExamNewR postCExamNewR tid ssh csh = do (cid, template) <- runDB $ do cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh template <- examTemplate cid return (cid, template) ((newExamResult, newExamWidget), newExamEnctype) <- runFormPost . validateForm validateExam $ examForm template formResult newExamResult $ \ExamForm{..} -> do insertRes <- runDBJobs $ do insertRes <- insertUnique Exam { examName = efName , examCourse = cid , examGradingRule = efGradingRule , examBonusRule = efBonusRule , examOccurrenceRule = efOccurrenceRule , examVisibleFrom = efVisibleFrom , examRegisterFrom = efRegisterFrom , examRegisterTo = efRegisterTo , examDeregisterUntil = efDeregisterUntil , examPublishOccurrenceAssignments = efPublishOccurrenceAssignments , examStart = efStart , examEnd = efEnd , examFinished = efFinished , examClosed = efClosed , examShowGrades = efShowGrades , examPublicStatistics = efPublicStatistics , examDescription = efDescription } whenIsJust insertRes $ \examid -> do insertMany_ [ ExamPart{..} | ExamPartForm{..} <- Set.toList efExamParts , let examPartExam = examid examPartName = epfName examPartMaxPoints = epfMaxPoints examPartWeight = epfWeight ] insertMany_ [ ExamOccurrence{..} | ExamOccurrenceForm{..} <- Set.toList efOccurrences , let examOccurrenceExam = examid examOccurrenceName = eofName examOccurrenceRoom = eofRoom examOccurrenceCapacity = eofCapacity examOccurrenceStart = eofStart examOccurrenceEnd = eofEnd examOccurrenceDescription = eofDescription ] let (invites, adds) = partitionEithers $ Set.toList efCorrectors insertMany_ [ ExamCorrector{..} | examCorrectorUser <- adds , let examCorrectorExam = examid ] sinkInvitationsF examCorrectorInvitationConfig $ map (, examid, (InvDBDataExamCorrector, InvTokenDataExamCorrector)) invites return insertRes case insertRes of Nothing -> addMessageI Error $ MsgExamNameTaken efName Just _ -> do addMessageI Success $ MsgExamCreated efName redirect $ CourseR tid ssh csh CExamListR let heading = prependCourseTitle tid ssh csh MsgExamNew siteLayoutMsg heading $ do setTitleI heading let newExamForm = wrapForm newExamWidget def { formMethod = POST , formAction = Just . SomeRoute $ CourseR tid ssh csh CExamNewR , formEncoding = newExamEnctype } $(widgetFile "exam-new") getEEditR, postEEditR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEEditR = postEEditR postEEditR tid ssh csh examn = do (cid, eId, template) <- runDB $ do (cid, exam@(Entity eId _)) <- fetchCourseIdExam tid ssh csh examn template <- examFormTemplate exam return (cid, eId, template) ((editExamResult, editExamWidget), editExamEnctype) <- runFormPost . validateForm validateExam . examForm $ Just template formResult editExamResult $ \ExamForm{..} -> do insertRes <- runDBJobs $ do insertRes <- myReplaceUnique eId Exam { examCourse = cid , examName = efName , examGradingRule = efGradingRule , examBonusRule = efBonusRule , examOccurrenceRule = efOccurrenceRule , examVisibleFrom = efVisibleFrom , examRegisterFrom = efRegisterFrom , examRegisterTo = efRegisterTo , examDeregisterUntil = efDeregisterUntil , examPublishOccurrenceAssignments = efPublishOccurrenceAssignments , examStart = efStart , examEnd = efEnd , examFinished = efFinished , examClosed = efClosed , examPublicStatistics = efPublicStatistics , examShowGrades = efShowGrades , examDescription = efDescription } when (is _Nothing insertRes) $ do occIds <- fmap catMaybes . forM (Set.toList efOccurrences) $ traverse decrypt . eofId deleteWhere [ ExamOccurrenceExam ==. eId, ExamOccurrenceId /<-. occIds ] forM_ (Set.toList efOccurrences) $ \case ExamOccurrenceForm{ eofId = Nothing, .. } -> insert_ ExamOccurrence { examOccurrenceExam = eId , examOccurrenceName = eofName , examOccurrenceRoom = eofRoom , examOccurrenceCapacity = eofCapacity , examOccurrenceStart = eofStart , examOccurrenceEnd = eofEnd , examOccurrenceDescription = eofDescription } ExamOccurrenceForm{ .. } -> void . runMaybeT $ do cID <- hoistMaybe eofId eofId' <- decrypt cID oldOcc <- MaybeT $ get eofId' guard $ examOccurrenceExam oldOcc == eId lift $ replace eofId' ExamOccurrence { examOccurrenceExam = eId , examOccurrenceName = eofName , examOccurrenceRoom = eofRoom , examOccurrenceCapacity = eofCapacity , examOccurrenceStart = eofStart , examOccurrenceEnd = eofEnd , examOccurrenceDescription = eofDescription } pIds <- fmap catMaybes . forM (Set.toList efExamParts) $ traverse decrypt . epfId deleteWhere [ ExamPartExam ==. eId, ExamPartId /<-. pIds ] forM_ (Set.toList efExamParts) $ \case ExamPartForm{ epfId = Nothing, .. } -> insert_ ExamPart { examPartExam = eId , examPartName = epfName , examPartMaxPoints = epfMaxPoints , examPartWeight = epfWeight } ExamPartForm{ .. } -> void . runMaybeT $ do cID <- hoistMaybe epfId epfId' <- decrypt cID oldPart <- MaybeT $ get epfId' guard $ examPartExam oldPart == eId lift $ replace epfId' ExamPart { examPartExam = eId , examPartName = epfName , examPartMaxPoints = epfMaxPoints , examPartWeight = epfWeight } let (invites, adds) = partitionEithers $ Set.toList efCorrectors deleteWhere [ ExamCorrectorExam ==. eId ] insertMany_ $ map (ExamCorrector eId) adds deleteWhere [ InvitationFor ==. invRef @ExamCorrector eId, InvitationEmail /<-. invites ] sinkInvitationsF examCorrectorInvitationConfig $ map (, eId, (InvDBDataExamCorrector, InvTokenDataExamCorrector)) invites return insertRes case insertRes of Just _ -> addMessageI Error $ MsgExamNameTaken efName Nothing -> do addMessageI Success $ MsgExamEdited efName redirect $ CExamR tid ssh csh efName EShowR let heading = prependCourseTitle tid ssh csh . MsgExamEditHeading $ efName template siteLayoutMsg heading $ do setTitleI heading let editExamForm = wrapForm editExamWidget def { formMethod = POST , formAction = Just . SomeRoute $ CExamR tid ssh csh examn EEditR , formEncoding = editExamEnctype } $(widgetFile "exam-edit") getEShowR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEShowR tid ssh csh examn = do cTime <- liftIO getCurrentTime mUid <- maybeAuthId (Entity _ Exam{..}, parts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, occurrences, (registered, mayRegister), occurrenceNamesShown) <- runDB $ do exam@(Entity eId Exam{..}) <- fetchExam tid ssh csh examn let examVisible = NTop (Just cTime) >= NTop examVisibleFrom let gradingVisible = NTop (Just cTime) >= NTop examFinished gradingShown <- or2M (return gradingVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR let occurrenceAssignmentsVisible = NTop (Just cTime) >= NTop examPublishOccurrenceAssignments occurrenceAssignmentsShown <- or2M (return occurrenceAssignmentsVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR parts <- selectList [ ExamPartExam ==. eId ] [ Asc ExamPartName ] resultsRaw <- for mUid $ \uid -> E.select . E.from $ \examPartResult -> do E.where_ $ examPartResult E.^. ExamPartResultUser E.==. E.val uid E.&&. examPartResult E.^. ExamPartResultExamPart `E.in_` E.valList (map entityKey parts) return examPartResult let results = maybe Map.empty (\rs -> Map.fromList [ (examPartResultExamPart, res) | res@(Entity _ ExamPartResult{..}) <- rs ]) resultsRaw result <- fmap join . for mUid $ getBy . UniqueExamResult eId occurrencesRaw <- E.select . E.from $ \examOccurrence -> do E.where_ $ examOccurrence E.^. ExamOccurrenceExam E.==. E.val eId let registered | Just uid <- mUid = E.exists . E.from $ \examRegistration -> do E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eId E.&&. examRegistration E.^. ExamRegistrationUser E.==. E.val uid E.&&. examRegistration E.^. ExamRegistrationOccurrence E.==. E.just (examOccurrence E.^. ExamOccurrenceId) | otherwise = E.false E.orderBy [E.desc registered, E.asc $ examOccurrence E.^. ExamOccurrenceStart, E.asc $ examOccurrence E.^. ExamOccurrenceRoom] return (examOccurrence, registered) let occurrences = map (over _2 E.unValue) occurrencesRaw registered <- for mUid $ existsBy . UniqueExamRegistration eId mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True occurrenceNamesShown <- hasReadAccessTo $ CExamR tid ssh csh examn EEditR return (exam, parts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, occurrences, (registered, mayRegister), occurrenceNamesShown) let examTimes = all (\(Entity _ ExamOccurrence{..}, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences registerWidget | Just isRegistered <- registered , mayRegister = Just $ do (examRegisterForm, examRegisterEnctype) <- liftHandlerT . generateFormPost . buttonForm' $ bool [BtnExamRegister] [BtnExamDeregister] isRegistered [whamlet|
$if isRegistered _{MsgExamRegistered} $else _{MsgExamNotRegistered} |] wrapForm examRegisterForm def { formAction = Just . SomeRoute $ CExamR tid ssh csh examName ERegisterR , formEncoding = examRegisterEnctype , formSubmit = FormNoSubmit } | fromMaybe False registered = Just [whamlet|_{MsgExamRegistered}|] | otherwise = Nothing let heading = prependCourseTitle tid ssh csh $ CI.original examName siteLayoutMsg heading $ do setTitleI heading let gradingKeyW :: [Points] -> Widget gradingKeyW bounds = let boundWidgets :: [Widget] boundWidgets = toWidget . (pack :: String -> Text) . showFixed True <$> 0 : bounds grades :: [ExamGrade] grades = universeF in $(widgetFile "widgets/gradingKey") examBonusW :: ExamBonusRule -> Widget examBonusW bonusRule = $(widgetFile "widgets/bonusRule") $(widgetFile "exam-show") type ExamUserTableExpr = (E.SqlExpr (Entity ExamRegistration) `E.InnerJoin` E.SqlExpr (Entity User)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamOccurrence)) `E.LeftOuterJoin` (E.SqlExpr (Maybe (Entity CourseParticipant)) `E.LeftOuterJoin` (E.SqlExpr (Maybe (Entity StudyFeatures)) `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree)) `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms)))) type ExamUserTableData = DBRow (Entity ExamRegistration, Entity User, Maybe (Entity ExamOccurrence), Maybe (Entity StudyFeatures), Maybe (Entity StudyDegree), Maybe (Entity StudyTerms)) instance HasEntity ExamUserTableData User where hasEntity = _dbrOutput . _2 instance HasUser ExamUserTableData where hasUser = _dbrOutput . _2 . _entityVal _userTableOccurrence :: Lens' ExamUserTableData (Maybe (Entity ExamOccurrence)) _userTableOccurrence = _dbrOutput . _3 queryUser :: ExamUserTableExpr -> E.SqlExpr (Entity User) queryUser = $(sqlIJproj 2 2) . $(sqlLOJproj 3 1) queryStudyFeatures :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures)) queryStudyFeatures = $(sqlIJproj 3 1) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3) queryExamRegistration :: ExamUserTableExpr -> E.SqlExpr (Entity ExamRegistration) queryExamRegistration = $(sqlIJproj 2 1) . $(sqlLOJproj 3 1) queryExamOccurrence :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity ExamOccurrence)) queryExamOccurrence = $(sqlLOJproj 3 2) queryStudyDegree :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree)) queryStudyDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3) queryStudyField :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms)) queryStudyField = $(sqlIJproj 3 3) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3) resultExamRegistration :: Lens' ExamUserTableData (Entity ExamRegistration) resultExamRegistration = _dbrOutput . _1 resultUser :: Lens' ExamUserTableData (Entity User) resultUser = _dbrOutput . _2 resultStudyFeatures :: Traversal' ExamUserTableData (Entity StudyFeatures) resultStudyFeatures = _dbrOutput . _4 . _Just resultStudyDegree :: Traversal' ExamUserTableData (Entity StudyDegree) resultStudyDegree = _dbrOutput . _5 . _Just resultStudyField :: Traversal' ExamUserTableData (Entity StudyTerms) resultStudyField = _dbrOutput . _6 . _Just resultExamOccurrence :: Traversal' ExamUserTableData (Entity ExamOccurrence) resultExamOccurrence = _dbrOutput . _3 . _Just data ExamUserTableCsv = ExamUserTableCsv { csvEUserSurname :: Maybe Text , csvEUserName :: Maybe Text , csvEUserMatriculation :: Maybe Text , csvEUserField :: Maybe Text , csvEUserDegree :: Maybe Text , csvEUserSemester :: Maybe Int , csvEUserOccurrence :: Maybe (CI Text) , csvEUserExercisePoints :: Maybe Points , csvEUserExercisePasses :: Maybe Int , csvEUserExercisePointsMax :: Maybe Points , csvEUserExercisePassesMax :: Maybe Int } deriving (Generic) examUserTableCsvOptions :: Csv.Options examUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 3 } instance ToNamedRecord ExamUserTableCsv where toNamedRecord = Csv.genericToNamedRecord examUserTableCsvOptions instance FromNamedRecord ExamUserTableCsv where parseNamedRecord = Csv.genericParseNamedRecord examUserTableCsvOptions instance DefaultOrdered ExamUserTableCsv where headerOrder = Csv.genericHeaderOrder examUserTableCsvOptions instance CsvColumnsExplained ExamUserTableCsv where csvColumnsExplanations = genericCsvColumnsExplanations examUserTableCsvOptions $ Map.fromList [ ('csvEUserSurname , MsgCsvColumnExamUserSurname ) , ('csvEUserName , MsgCsvColumnExamUserName ) , ('csvEUserMatriculation , MsgCsvColumnExamUserMatriculation ) , ('csvEUserField , MsgCsvColumnExamUserField ) , ('csvEUserDegree , MsgCsvColumnExamUserDegree ) , ('csvEUserSemester , MsgCsvColumnExamUserSemester ) , ('csvEUserOccurrence , MsgCsvColumnExamUserOccurrence ) , ('csvEUserExercisePoints , MsgCsvColumnExamUserExercisePoints ) , ('csvEUserExercisePasses , MsgCsvColumnExamUserExercisePasses ) , ('csvEUserExercisePointsMax, MsgCsvColumnExamUserExercisePointsMax ) , ('csvEUserExercisePassesMax, MsgCsvColumnExamUserExercisePassesMax ) ] data ExamUserAction = ExamUserDeregister | ExamUserAssignOccurrence deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) instance Universe ExamUserAction instance Finite ExamUserAction nullaryPathPiece ''ExamUserAction $ camelToPathPiece' 2 embedRenderMessage ''UniWorX ''ExamUserAction id data ExamUserActionData = ExamUserDeregisterData | ExamUserAssignOccurrenceData (Maybe ExamOccurrenceId) data ExamUserCsvActionClass = ExamUserCsvCourseRegister | ExamUserCsvRegister | ExamUserCsvAssignOccurrence | ExamUserCsvSetCourseField | ExamUserCsvDeregister deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) embedRenderMessage ''UniWorX ''ExamUserCsvActionClass id data ExamUserCsvAction = ExamUserCsvCourseRegisterData { examUserCsvActUser :: UserId , examUserCsvActCourseField :: Maybe StudyFeaturesId , examUserCsvActOccurrence :: Maybe ExamOccurrenceId } | ExamUserCsvRegisterData { examUserCsvActUser :: UserId , examUserCsvActOccurrence :: Maybe ExamOccurrenceId } | ExamUserCsvAssignOccurrenceData { examUserCsvActRegistration :: ExamRegistrationId , examUserCsvActOccurrence :: Maybe ExamOccurrenceId } | ExamUserCsvSetCourseFieldData { examUserCsvActCourseParticipant :: CourseParticipantId , examUserCsvActCourseField :: Maybe StudyFeaturesId } | ExamUserCsvDeregisterData { examUserCsvActRegistration :: ExamRegistrationId } deriving (Eq, Ord, Read, Show, Generic, Typeable) deriveJSON defaultOptions { constructorTagModifier = over Text.packed $ Text.intercalate "-" . map Text.toLower . drop 3 . dropEnd 1 . splitCamel , fieldLabelModifier = camelToPathPiece' 3 , sumEncoding = TaggedObject "action" "data" } ''ExamUserCsvAction data ExamUserCsvException = ExamUserCsvExceptionNoMatchingUser | ExamUserCsvExceptionNoMatchingStudyFeatures | ExamUserCsvExceptionNoMatchingOccurrence deriving (Show, Generic, Typeable) instance Exception ExamUserCsvException embedRenderMessage ''UniWorX ''ExamUserCsvException id getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEUsersR = postEUsersR postEUsersR tid ssh csh examn = do (registrationResult, examUsersTable) <- runDB $ do exam@(Entity eid Exam{..}) <- fetchExam tid ssh csh examn bonus <- examBonus exam let allBoni = (mappend <$> normalSummary <*> bonusSummary) $ fold bonus showPasses = numSheetsPasses allBoni /= 0 showPoints = getSum (numSheetsPoints allBoni) /= 0 let examUsersDBTable = DBTable{..} where dbtSQLQuery ((examRegistration `E.InnerJoin` user) `E.LeftOuterJoin` occurrence `E.LeftOuterJoin` (courseParticipant `E.LeftOuterJoin` (studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyField))) = do E.on $ studyField E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree E.on $ studyFeatures E.?. StudyFeaturesId E.==. E.joinV (courseParticipant E.?. CourseParticipantField) E.on $ courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val examCourse) E.&&. courseParticipant E.?. CourseParticipantUser E.==. E.just (user E.^. UserId) E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid) E.&&. occurrence E.?. ExamOccurrenceId E.==. examRegistration E.^. ExamRegistrationOccurrence E.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eid return (examRegistration, user, occurrence, studyFeatures, studyDegree, studyField) dbtRowKey = queryExamRegistration >>> (E.^. ExamRegistrationId) dbtProj = return dbtColonnade = mconcat $ catMaybes [ pure $ dbSelect (applying _2) id $ return . view (resultExamRegistration . _entityKey) , pure $ colUserNameLink (CourseR tid ssh csh . CUserR) , pure colUserMatriclenr , pure $ colField resultStudyField , pure $ colDegreeShort resultStudyDegree , pure $ colFeaturesSemester resultStudyFeatures , pure $ sortable (Just "occurrence") (i18nCell MsgExamOccurrence) $ maybe mempty (anchorCell' (\n -> CExamR tid ssh csh examn EShowR :#: [st|exam-occurrence__#{n}|]) id . examOccurrenceName . entityVal) . view _userTableOccurrence , guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) -> fromMaybe mempty $ do SheetGradeSummary{achievedPasses} <- examBonusAchieved uid bonus SheetGradeSummary{numSheetsPasses} <- examBonusPossible uid bonus return $ propCell (getSum achievedPasses) (getSum numSheetsPasses) , guardOn showPoints $ sortable Nothing (i18nCell MsgAchievedPoints) $ \(view $ resultUser . _entityKey -> uid) -> fromMaybe mempty $ do SheetGradeSummary{achievedPoints} <- examBonusAchieved uid bonus SheetGradeSummary{sumSheetsPoints} <- examBonusPossible uid bonus return $ propCell (getSum achievedPoints) (getSum sumSheetsPoints) ] dbtSorting = Map.fromList [ sortUserNameLink queryUser , sortUserSurname queryUser , sortUserDisplayName queryUser , sortUserMatriclenr queryUser , sortField queryStudyField , sortDegreeShort queryStudyDegree , sortFeaturesSemester queryStudyFeatures , ("occurrence", SortColumn $ queryExamOccurrence >>> (E.?. ExamOccurrenceName)) ] dbtFilter = Map.fromList [ fltrUserNameEmail queryUser , fltrUserMatriclenr queryUser , fltrField queryStudyField , fltrDegree queryStudyDegree , fltrFeaturesSemester queryStudyFeatures , ("occurrence", FilterColumn . E.mkContainsFilterWith Just $ queryExamOccurrence >>> (E.?. ExamOccurrenceName)) ] dbtFilterUI mPrev = mconcat [ fltrUserNameEmailUI mPrev , fltrUserMatriclenrUI mPrev , fltrFieldUI mPrev , fltrDegreeUI mPrev , fltrFeaturesSemesterUI mPrev , prismAForm (singletonFilter "occurrence") mPrev $ aopt textField (fslI MsgExamOccurrence) ] dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } dbtParams = DBParamsForm { dbParamsFormMethod = POST , dbParamsFormAction = Just . SomeRoute $ CExamR tid ssh csh examn EUsersR , dbParamsFormAttrs = [] , dbParamsFormSubmit = FormSubmit , dbParamsFormAdditional = \csrf -> do let actionMap :: Map ExamUserAction (AForm Handler ExamUserActionData) actionMap = Map.fromList [ ( ExamUserDeregister , pure ExamUserDeregisterData ) , ( ExamUserAssignOccurrence , ExamUserAssignOccurrenceData <$> aopt (examOccurrenceField eid) (fslI MsgExamOccurrence) (Just Nothing) ) ] (res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf let formRes = (, mempty) . First . Just <$> res return (formRes, formWgt) , dbParamsFormEvaluate = liftHandlerT . runFormPost , dbParamsFormResult = id , dbParamsFormIdent = def } dbtIdent :: Text dbtIdent = "exam-users" dbtCsvEncode :: DBTCsvEncode ExamUserTableData ExamUserTableCsv dbtCsvEncode = DictJust . C.map $ ExamUserTableCsv <$> view (resultUser . _entityVal . _userSurname . to Just) <*> view (resultUser . _entityVal . _userDisplayName . to Just) <*> view (resultUser . _entityVal . _userMatrikelnummer) <*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just) <*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just) <*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester) <*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName) <*> preview (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _Just . _achievedPoints . _Wrapped) <*> preview (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _Just . _achievedPasses . _Wrapped . integral) <*> preview (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _Just . _sumSheetsPoints . _Wrapped) <*> preview (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _Just . _numSheetsPasses . _Wrapped . integral) dbtCsvDecode = Just DBTCsvDecode { dbtCsvRowKey = \csv -> do uid <- lift $ view _2 <$> guessUser csv fmap E.Value . MaybeT . getKeyBy $ UniqueExamRegistration eid uid , dbtCsvComputeActions = \case DBCsvDiffMissing{dbCsvOldKey} -> yield . ExamUserCsvDeregisterData $ E.unValue dbCsvOldKey DBCsvDiffNew{dbCsvNewKey = Just _} -> fail "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys" DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do (isPart, uid) <- lift $ guessUser dbCsvNew if | isPart -> do yieldM $ ExamUserCsvRegisterData <$> pure uid <*> lookupOccurrence dbCsvNew newFeatures <- lift $ lookupStudyFeatures dbCsvNew Just (Entity cpId CourseParticipant{ courseParticipantField = oldFeatures }) <- lift . getBy $ UniqueParticipant uid examCourse when (newFeatures /= oldFeatures) $ yield $ ExamUserCsvSetCourseFieldData cpId newFeatures | otherwise -> yieldM $ ExamUserCsvCourseRegisterData uid <$> lookupStudyFeatures dbCsvNew <*> lookupOccurrence dbCsvNew DBCsvDiffExisting{..} -> do newOccurrence <- lift $ lookupOccurrence dbCsvNew when (newOccurrence /= dbCsvOld ^? resultExamOccurrence . _entityKey) $ yield $ ExamUserCsvAssignOccurrenceData (E.unValue dbCsvOldKey) newOccurrence newFeatures <- lift $ lookupStudyFeatures dbCsvNew when (newFeatures /= dbCsvOld ^? resultStudyFeatures . _entityKey) $ do Just (Entity cpId _) <- lift . getBy . flip UniqueParticipant examCourse $ dbCsvOld ^. resultUser . _entityKey yield $ ExamUserCsvSetCourseFieldData cpId newFeatures , dbtCsvClassifyAction = \case ExamUserCsvCourseRegisterData{} -> ExamUserCsvCourseRegister ExamUserCsvRegisterData{} -> ExamUserCsvRegister ExamUserCsvDeregisterData{} -> ExamUserCsvDeregister ExamUserCsvAssignOccurrenceData{} -> ExamUserCsvAssignOccurrence ExamUserCsvSetCourseFieldData{} -> ExamUserCsvSetCourseField , dbtCsvCoarsenActionClass = \case ExamUserCsvCourseRegister -> DBCsvActionNew ExamUserCsvRegister -> DBCsvActionNew ExamUserCsvDeregister -> DBCsvActionMissing _other -> DBCsvActionExisting , dbtCsvExecuteActions = do C.mapM_ $ \case ExamUserCsvCourseRegisterData{..} -> do now <- liftIO getCurrentTime insert_ CourseParticipant { courseParticipantCourse = examCourse , courseParticipantUser = examUserCsvActUser , courseParticipantRegistration = now , courseParticipantField = examUserCsvActCourseField } User{userIdent} <- getJust examUserCsvActUser audit' $ TransactionExamRegister (unTermKey tid) (unSchoolKey ssh) csh examn userIdent insert_ ExamRegistration { examRegistrationExam = eid , examRegistrationUser = examUserCsvActUser , examRegistrationOccurrence = examUserCsvActOccurrence , examRegistrationTime = now } ExamUserCsvRegisterData{..} -> do examRegistrationTime <- liftIO getCurrentTime insert_ ExamRegistration { examRegistrationExam = eid , examRegistrationUser = examUserCsvActUser , examRegistrationOccurrence = examUserCsvActOccurrence , .. } ExamUserCsvAssignOccurrenceData{..} -> update examUserCsvActRegistration [ ExamRegistrationOccurrence =. examUserCsvActOccurrence ] ExamUserCsvSetCourseFieldData{..} -> update examUserCsvActCourseParticipant [ CourseParticipantField =. examUserCsvActCourseField ] ExamUserCsvDeregisterData{..} -> do ExamRegistration{examRegistrationUser} <- getJust examUserCsvActRegistration User{userIdent} <- getJust examRegistrationUser audit' $ TransactionExamDeregister (unTermKey tid) (unSchoolKey ssh) csh examn userIdent delete examUserCsvActRegistration return $ CExamR tid ssh csh examn EUsersR , dbtCsvRenderKey = \(registeredUserName -> registeredUserName') -> \case ExamUserCsvCourseRegisterData{..} -> do (User{..}, occ) <- liftHandlerT . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust [whamlet| $newline never ^{nameWidget userDisplayName userSurname} $maybe features <- examUserCsvActCourseField , ^{studyFeaturesWidget features} $nothing , _{MsgCourseStudyFeatureNone} $maybe ExamOccurrence{examOccurrenceName} <- occ \ (#{examOccurrenceName}) $nothing \ (_{MsgExamNoOccurrence}) |] ExamUserCsvRegisterData{..} -> do (User{..}, occ) <- liftHandlerT . runDB $ (,) <$> getJust examUserCsvActUser <*> for examUserCsvActOccurrence getJust [whamlet| $newline never ^{nameWidget userDisplayName userSurname} $maybe ExamOccurrence{examOccurrenceName} <- occ \ (#{examOccurrenceName}) $nothing \ (_{MsgExamNoOccurrence}) |] ExamUserCsvAssignOccurrenceData{..} -> do occ <- for examUserCsvActOccurrence $ liftHandlerT . runDB . getJust [whamlet| $newline never ^{registeredUserName' examUserCsvActRegistration} $maybe ExamOccurrence{examOccurrenceName} <- occ \ (#{examOccurrenceName}) $nothing \ (_{MsgExamNoOccurrence}) |] ExamUserCsvSetCourseFieldData{..} -> do User{..} <- liftHandlerT . runDB $ getJust . courseParticipantUser =<< getJust examUserCsvActCourseParticipant [whamlet| $newline never ^{nameWidget userDisplayName userSurname} $maybe features <- examUserCsvActCourseField , ^{studyFeaturesWidget features} $nothing , _{MsgCourseStudyFeatureNone} |] ExamUserCsvDeregisterData{..} -> registeredUserName' examUserCsvActRegistration , dbtCsvRenderActionClass = toWidget <=< ap getMessageRender . pure , dbtCsvRenderException = ap getMessageRender . pure :: ExamUserCsvException -> DB Text } where studyFeaturesWidget :: StudyFeaturesId -> Widget studyFeaturesWidget featId = do (StudyFeatures{studyFeaturesSemester}, (degree, terms)) <- liftHandlerT . runDB . ($ featId) . runKleisli $ Kleisli getJust >>> Kleisli return &&& Kleisli (getJust . studyFeaturesDegree) &&& Kleisli (getJust . studyFeaturesField) [whamlet| $newline never _{StudyDegreeTerm degree terms}, _{MsgStudyFeatureAge} #{studyFeaturesSemester} |] registeredUserName :: Map (E.Value ExamRegistrationId) ExamUserTableData -> ExamRegistrationId -> Widget registeredUserName existing (E.Value -> registration) = nameWidget userDisplayName userSurname where Entity _ User{..} = view resultUser $ existing ! registration guessUser :: ExamUserTableCsv -> DB (Bool, UserId) guessUser ExamUserTableCsv{..} = $cachedHereBinary (csvEUserMatriculation, csvEUserName, csvEUserSurname) $ do users <- E.select . E.from $ \user -> do E.where_ . E.and $ catMaybes [ (user E.^. UserMatrikelnummer E.==.) . E.val . Just <$> csvEUserMatriculation , (user E.^. UserDisplayName E.==.) . E.val <$> csvEUserName , (user E.^. UserSurname E.==.) . E.val <$> csvEUserSurname ] let isCourseParticipant = E.exists . E.from $ \courseParticipant -> E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. E.val examCourse E.&&. courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId E.limit 2 return $ (isCourseParticipant, user E.^. UserId) case users of (filter . view $ _1 . _Value -> [(E.Value isPart, E.Value uid)]) -> return (isPart, uid) [(E.Value isPart, E.Value uid)] -> return (isPart, uid) _other -> throwM ExamUserCsvExceptionNoMatchingUser lookupOccurrence :: ExamUserTableCsv -> DB (Maybe ExamOccurrenceId) lookupOccurrence ExamUserTableCsv{..} = $cachedHereBinary (CI.foldedCase <$> csvEUserOccurrence) . for csvEUserOccurrence $ \occName -> do occIds <- selectKeysList [ ExamOccurrenceName ==. occName, ExamOccurrenceExam ==. eid ] [] case occIds of [occId] -> return occId _other -> throwM ExamUserCsvExceptionNoMatchingOccurrence lookupStudyFeatures :: ExamUserTableCsv -> DB (Maybe StudyFeaturesId) lookupStudyFeatures csv@ExamUserTableCsv{..} = do uid <- view _2 <$> guessUser csv studyFeatures <- E.select . E.from $ \(studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyTerms) -> do E.on $ studyTerms E.^. StudyTermsId E.==. studyFeatures E.^. StudyFeaturesField E.on $ studyDegree E.^. StudyDegreeId E.==. studyFeatures E.^. StudyFeaturesDegree E.where_ . E.and $ catMaybes [ do field <- csvEUserField return . E.or $ catMaybes [ Just $ studyTerms E.^. StudyTermsName `E.ciEq` E.just (E.val field) , Just $ studyTerms E.^. StudyTermsShorthand `E.ciEq` E.just (E.val field) , (studyTerms E.^. StudyTermsKey E.==.) . E.val <$> readMay field ] , do degree <- csvEUserDegree return . E.or $ catMaybes [ Just $ studyDegree E.^. StudyDegreeName `E.ciEq` E.just (E.val degree) , Just $ studyDegree E.^. StudyDegreeShorthand `E.ciEq` E.just (E.val degree) , (studyDegree E.^. StudyDegreeKey E.==.) . E.val <$> readMay degree ] , (studyFeatures E.^. StudyFeaturesSemester E.==.) . E.val <$> csvEUserSemester ] E.where_ $ studyFeatures E.^. StudyFeaturesUser E.==. E.val uid E.&&. studyFeatures E.^. StudyFeaturesType E.==. E.val FieldPrimary E.&&. studyFeatures E.^. StudyFeaturesValid E.==. E.val True E.limit 2 return $ studyFeatures E.^. StudyFeaturesId case studyFeatures of [E.Value fid] -> return $ Just fid _other | is _Nothing csvEUserField , is _Nothing csvEUserDegree , is _Nothing csvEUserSemester -> return Nothing _other -> throwM ExamUserCsvExceptionNoMatchingStudyFeatures examUsersDBTableValidator = def postprocess :: FormResult (First ExamUserActionData, DBFormResult ExamRegistrationId Bool ExamUserTableData) -> FormResult (ExamUserActionData, Set ExamRegistrationId) postprocess inp = do (First (Just act), regMap) <- inp let regSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) regMap return (act, regSet) over _1 postprocess <$> dbTable examUsersDBTableValidator examUsersDBTable formResult registrationResult $ \case (ExamUserDeregisterData, selectedRegistrations) -> do nrDel <- runDB $ deleteWhereCount [ ExamRegistrationId <-. Set.toList selectedRegistrations ] addMessageI Success $ MsgExamUsersDeregistered nrDel redirect $ CExamR tid ssh csh examn EUsersR (ExamUserAssignOccurrenceData occId, selectedRegistrations) -> do nrUpdated <- runDB $ updateWhereCount [ ExamRegistrationId <-. Set.toList selectedRegistrations ] [ ExamRegistrationOccurrence =. occId ] addMessageI Success $ MsgExamUsersOccurrenceUpdated nrUpdated redirect $ CExamR tid ssh csh examn EUsersR siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamUsersHeading) $ do setTitleI $ prependCourseTitle tid ssh csh MsgExamUsersHeading $(widgetFile "exam-users") getEAddUserR, postEAddUserR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEAddUserR = postEAddUserR postEAddUserR = error "postEAddUserR" getEInviteR, postEInviteR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEInviteR = postEInviteR postEInviteR = error "postEInviteR" postERegisterR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html postERegisterR tid ssh csh examn = do Entity uid User{..} <- requireAuth Entity eId Exam{..} <- runDB $ fetchExam tid ssh csh examn ((btnResult, _), _) <- runFormPost buttonForm formResult btnResult $ \case BtnExamRegister -> do runDB $ do now <- liftIO getCurrentTime insert_ $ ExamRegistration eId uid Nothing now audit' $ TransactionExamRegister (unTermKey tid) (unSchoolKey ssh) csh examn userIdent addMessageWidget Success [whamlet|