diff --git a/src/Handler/Course.hs b/src/Handler/Course.hs index 8be5f3094..c7e8c7cb6 100644 --- a/src/Handler/Course.hs +++ b/src/Handler/Course.hs @@ -1,1450 +1,20 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} - -module Handler.Course where +module Handler.Course + ( module Handler.Course + ) where import Import -import Utils.Lens -import Utils.Form --- import Utils.DB -import Handler.Utils -import Handler.Utils.Course -import Handler.Utils.Tutorial -import Handler.Utils.Communication -import Handler.Utils.Delete -import Handler.Utils.Database -import Handler.Utils.Table.Cells -import Handler.Utils.Table.Columns -import Handler.Utils.Invitations -import Database.Persist.Sql (deleteWhereCount) -import qualified Database.Esqueleto.Utils as E -import Database.Esqueleto.Utils.TH --- import Data.Time -import qualified Data.CaseInsensitive as CI -import Data.Function ((&)) --- import Yesod.Form.Bootstrap3 - -import Data.Maybe (fromJust) -import qualified Data.Set as Set -import Data.Map ((!)) -import qualified Data.Map as Map - -import qualified Database.Esqueleto as E - -import Text.Blaze.Html.Renderer.Text (renderHtml) - -import Jobs.Queue - -import Data.Aeson hiding (Result(..)) - -import Text.Hamlet (ihamlet) - -import Control.Monad.Trans.Writer (WriterT, execWriterT) -import Control.Monad.Except (MonadError(..)) - -import Generics.Deriving.Monoid (memptydefault, mappenddefault) - - --- Dedicated CourseRegistrationButton -data ButtonCourseRegister = BtnCourseRegister | BtnCourseDeregister - deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) -instance Universe ButtonCourseRegister -instance Finite ButtonCourseRegister -nullaryPathPiece ''ButtonCourseRegister $ camelToPathPiece' 1 -embedRenderMessage ''UniWorX ''ButtonCourseRegister id -instance Button UniWorX ButtonCourseRegister where - btnClasses BtnCourseRegister = [BCIsButton, BCPrimary] - btnClasses BtnCourseDeregister = [BCIsButton, BCDanger] - - btnLabel BtnCourseRegister = [whamlet|#{iconEnrol True} _{MsgBtnCourseRegister}|] - btnLabel BtnCourseDeregister = [whamlet|#{iconEnrol False} _{MsgBtnCourseDeregister}|] - - --- NOTE: Outdated way to use dbTable; see ProfileDataR Handler for a more recent method. -type CourseTableData = DBRow (Entity Course, Int, Bool, Entity School) - -colCourse :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) -colCourse = sortable (Just "course") (i18nCell MsgCourse) - $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> - anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) - [whamlet|_{courseName}|] - --- colCourseDescr :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) --- colCourseDescr = sortable (Just "course") (i18nCell MsgCourse) $ do --- course <- view $ _dbrOutput . _1 . _entityVal --- return $ courseCell course - -colDescription :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) -colDescription = sortable Nothing mempty - $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> - case courseDescription of - Nothing -> mempty - (Just descr) -> cell $ modal (toWidget $ hasComment True) (Right $ toWidget descr) - -colCShort :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) -colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort) - $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> - anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|_{courseShorthand}|] - --- colCShortDescr :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) --- colCShortDescr = sortable (Just "cshort") (i18nCell MsgCourseShort) --- $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> mappend --- ( anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|#{display courseShorthand}|] ) --- ( case courseDescription of --- Nothing -> mempty --- (Just descr) -> cell --- [whamlet| --- $newline never ---
--- ^{modal "Beschreibung" (Right $ toWidget descr)} --- |] --- ) - -colTerm :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) -colTerm = sortable (Just "term") (i18nCell MsgTerm) - $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> - anchorCell (TermCourseListR courseTerm) [whamlet|#{courseTerm}|] - -colSchool :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) -colSchool = sortable (Just "school") (i18nCell MsgCourseSchool) - $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } -> - anchorCell (TermSchoolCourseListR courseTerm courseSchool) [whamlet|_{schoolName}|] - -colSchoolShort :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) -colSchoolShort = sortable (Just "schoolshort") (i18nCell MsgCourseSchoolShort) - $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } -> - anchorCell (TermSchoolCourseListR courseTerm courseSchool) [whamlet|_{schoolShorthand}|] - -colRegFrom :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) -colRegFrom = sortable (Just "register-from") (i18nCell MsgRegisterFrom) - $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> - maybe mempty dateTimeCell courseRegisterFrom - -- cell $ traverse (formatTime SelFormatDateTime) courseRegisterFrom >>= maybe mempty toWidget - -colRegTo :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) -colRegTo = sortable (Just "register-to") (i18nCell MsgRegisterTo) - $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> - maybe mempty dateTimeCell courseRegisterTo - -colMembers :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) -colMembers = sortable (Just "members") (i18nCell MsgCourseMembers) - $ \DBRow{ dbrOutput=(Entity _ Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of - Nothing -> MsgCourseMembersCount currentParticipants - Just limit -> MsgCourseMembersCountLimited currentParticipants limit - -colRegistered :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) -colRegistered = sortable (Just "registered") (i18nCell MsgRegistered) - $ \DBRow{ dbrOutput=(_, _, registered, _) } -> tickmarkCell registered - -type CourseTableExpr = E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity School) - -course2Participants :: CourseTableExpr -> E.SqlExpr (E.Value Int) -course2Participants (course `E.InnerJoin` _school) = E.sub_select . E.from $ \courseParticipant -> do - E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. course E.^. CourseId - return (E.countRows :: E.SqlExpr (E.Value Int)) - -course2Registered :: Maybe UserId -> CourseTableExpr -> E.SqlExpr (E.Value Bool) -course2Registered muid (course `E.InnerJoin` _school) = E.exists . E.from $ \courseParticipant -> - E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. course E.^. CourseId - E.&&. E.just (courseParticipant E.^. CourseParticipantUser) E.==. E.val muid - -makeCourseTable :: ( IsDBTable m x, ToSortable h, Functor h, DBResult m x ~ ((), Widget) ) - => _ -> Colonnade h CourseTableData (DBCell m x) -> PSValidator m x -> DB Widget -makeCourseTable whereClause colChoices psValidator = do - muid <- lift maybeAuthId - let dbtSQLQuery :: CourseTableExpr -> E.SqlQuery _ - dbtSQLQuery qin@(course `E.InnerJoin` school) = do - E.on $ course E.^. CourseSchool E.==. school E.^. SchoolId - let participants = course2Participants qin - let registered = course2Registered muid qin - E.where_ $ whereClause (course, participants, registered) - return (course, participants, registered, school) - dbtProj :: DBRow _ -> MaybeT (ReaderT SqlBackend (HandlerT UniWorX IO)) CourseTableData - dbtProj = traverse $ \(course, E.Value participants, E.Value registered, school) -> return (course, participants, registered, school) - snd <$> dbTable psValidator DBTable - { dbtSQLQuery - , dbtRowKey = \(course `E.InnerJoin` _) -> course E.^. CourseId - , dbtColonnade = colChoices - , dbtProj - , dbtSorting = Map.fromList -- OverloadedLists does not work with the templates here - [ ( "course", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseName) - , ( "cshort", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseShorthand) - , ( "term" , SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseTerm) - , ( "school", SortColumn $ \(_course `E.InnerJoin` school) -> school E.^. SchoolName) - , ( "schoolshort", SortColumn $ \(_course `E.InnerJoin` school) -> school E.^. SchoolShorthand) - , ( "register-from", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterFrom) - , ( "register-to", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterTo) - , ( "members", SortColumn course2Participants ) - , ( "registered", SortColumn $ course2Registered muid) - ] - , dbtFilter = Map.fromList -- OverloadedLists does not work with the templates here - [ ( "course", FilterColumn $ \(course `E.InnerJoin` _school:: CourseTableExpr) criterias -> if - | Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool) - | otherwise -> course E.^. CourseName `E.in_` E.valList (Set.toList criterias) - ) - , ( "cshort", FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterias -> if - | Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool) - | otherwise -> course E.^. CourseShorthand `E.in_` E.valList (Set.toList criterias) - ) - , ( "term" , FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterias -> if - | Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool) - | otherwise -> course E.^. CourseTerm `E.in_` E.valList (Set.toList criterias) - ) --- , ( "school", FilterColumn $ \(_course `E.InnerJoin` school :: CourseTableExpr) criterias -> if --- | Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool) --- | otherwise -> school E.^. SchoolName `E.in_` E.valList (Set.toList criterias) --- ) - , ( "school", FilterColumn $ \(_course `E.InnerJoin` school :: CourseTableExpr) -> - emptyOrIn $ school E.^. SchoolName -- TODO: Refactor all?! - ) - , ( "schoolshort", FilterColumn $ \(_course `E.InnerJoin` school :: CourseTableExpr) criterias -> if - | Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool) - | otherwise -> school E.^. SchoolShorthand `E.in_` E.valList (Set.toList criterias) - ) - , ( "registered", FilterColumn $ \tExpr criterion -> case getLast (criterion :: Last Bool) of - Nothing -> E.val True :: E.SqlExpr (E.Value Bool) - Just needle -> course2Registered muid tExpr E.==. E.val needle - ) - , ( "search", FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterion -> case getLast (criterion :: Last Text) of - Nothing -> E.val True :: E.SqlExpr (E.Value Bool) - Just needle -> (E.castString (course E.^. CourseName) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) - E.||. (E.castString (course E.^. CourseShorthand) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) - E.||. (E.castString (course E.^. CourseDescription) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) - ) - ] - , dbtFilterUI = \mPrev -> mconcat $ catMaybes - [ Just $ prismAForm (singletonFilter "search") mPrev $ aopt textField (fslI MsgCourseFilterSearch) - , muid $> prismAForm (singletonFilter "registered" . maybePrism _PathPiece) mPrev (aopt boolField (fslI MsgCourseFilterRegistered)) - ] - , dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } - , dbtParams = def - , dbtIdent = "courses" :: Text - , dbtCsvEncode = noCsvEncode - , dbtCsvDecode = Nothing - } - -getCourseListR :: Handler Html -getCourseListR = do - muid <- maybeAuthId - let colonnade = widgetColonnade $ mconcat - [ colCourse -- colCourseDescr - , colDescription - , colSchoolShort - , colTerm - , colCShort - , maybe mempty (const colRegistered) muid - ] - whereClause = const $ E.val True - validator = def - & defaultSorting [SortDescBy "term",SortAscBy "course"] - coursesTable <- runDB $ makeCourseTable whereClause colonnade validator - defaultLayout $ do - setTitleI MsgCourseListTitle - $(widgetFile "courses") - -getTermCurrentR :: Handler Html -getTermCurrentR = do - termIds <- runDB $ selectKeysList [TermActive ==. True] [Desc TermName] - case fromNullable termIds of - Nothing -> notFound - (Just (maximum -> tid)) -> - redirect $ TermCourseListR tid -- redirect avoids problematic breadcrumbs, headings, etc. - -getTermSchoolCourseListR :: TermId -> SchoolId -> Handler Html -getTermSchoolCourseListR tid ssh = do - void . runDB $ get404 tid -- Just ensure the term exists - School{schoolName=school} <- runDB $ get404 ssh -- Just ensure the term exists - muid <- maybeAuthId - let colonnade = widgetColonnade $ mconcat - [ dbRow - , colCShort - , colDescription - , colRegFrom - , colRegTo - , colMembers - , maybe mempty (const colRegistered) muid - ] - whereClause (course, _, _) = - course E.^. CourseTerm E.==. E.val tid - E.&&. course E.^. CourseSchool E.==. E.val ssh - validator = def - & defaultSorting [SortAscBy "cshort"] - coursesTable <- runDB $ makeCourseTable whereClause colonnade validator - defaultLayout $ do - setTitleI $ MsgTermSchoolCourseListTitle tid school - $(widgetFile "courses") - - -getTermCourseListR :: TermId -> Handler Html -getTermCourseListR tid = do - void . runDB $ get404 tid -- Just ensure the term exists - muid <- maybeAuthId - let colonnade = widgetColonnade $ mconcat - [ dbRow - , colCShort - , colDescription - , colSchoolShort - , colRegFrom - , colRegTo - , colMembers - , maybe mempty (const colRegistered) muid - ] - whereClause (course, _, _) = course E.^. CourseTerm E.==. E.val tid - validator = def - & defaultSorting [SortAscBy "cshort"] - coursesTable <- runDB $ makeCourseTable whereClause colonnade validator - defaultLayout $ do - setTitleI . MsgTermCourseListTitle $ tid - $(widgetFile "courses") - -getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html -getCShowR tid ssh csh = do - mbAid <- maybeAuthId - (cid,course,schoolName,participants,registration,defSFid,lecturers,assistants,correctors,tutors) <- runDB . maybeT notFound $ do - [(E.Entity cid course, E.Value schoolName, E.Value participants, fmap entityVal -> registration)] - <- lift . E.select . E.from $ - \((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do - E.on $ E.just (course E.^. CourseId) E.==. participant E.?. CourseParticipantCourse - E.&&. E.val mbAid E.==. participant E.?. CourseParticipantUser - E.on $ course E.^. CourseSchool E.==. school E.^. SchoolId - E.where_ $ course E.^. CourseTerm E.==. E.val tid - E.&&. course E.^. CourseSchool E.==. E.val ssh - E.&&. course E.^. CourseShorthand E.==. E.val csh - E.limit 1 -- we know that there is at most one match, but we tell the DB this info too - let numParticipants = E.sub_select . E.from $ \part -> do - E.where_ $ part E.^. CourseParticipantCourse E.==. course E.^. CourseId - return ( E.countRows :: E.SqlExpr (E.Value Int)) - return (course,school E.^. SchoolName, numParticipants, participant) - defSFid <- ifMaybeM mbAid Nothing $ \uid -> lift $ selectFirst [StudyFeaturesUser ==. uid, StudyFeaturesType ==. FieldPrimary, StudyFeaturesValid ==. True] [Desc StudyFeaturesUpdated, Desc StudyFeaturesDegree, Desc StudyFeaturesField] -- sorting by degree & field is an heuristic only, but this is okay for a default suggestion - staff <- lift . E.select $ E.from $ \(lecturer `E.InnerJoin` user) -> do - E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId - E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid - E.orderBy [ E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName ] - return ( lecturer E.^. LecturerType - , user E.^. UserEmail, user E.^. UserDisplayName, user E.^. UserSurname) - let partStaff :: (LecturerType, UserEmail, Text, Text) -> Either (UserEmail, Text, Text) (UserEmail, Text, Text) - partStaff (CourseLecturer ,name,surn,mail) = Right (name,surn,mail) - partStaff (_courseAssistant,name,surn,mail) = Left (name,surn,mail) - (assistants,lecturers) = partitionWith partStaff $ map $(unValueN 4) staff - correctors <- fmap (map $(unValueN 3)) . lift . E.select $ E.from $ \(sheet `E.InnerJoin` sheetCorrector `E.InnerJoin` user) -> E.distinctOnOrderBy [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName, E.asc $ user E.^. UserEmail ] $ do - E.on $ sheetCorrector E.^. SheetCorrectorUser E.==. user E.^. UserId - E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId - E.where_ $ sheet E.^. SheetCourse E.==. E.val cid - E.orderBy [ E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName ] - return ( user E.^. UserEmail, user E.^. UserDisplayName, user E.^. UserSurname ) - tutors <- fmap (map $(unValueN 3)) . lift . E.select $ E.from $ \(tutorial `E.InnerJoin` tutor `E.InnerJoin` user) -> E.distinctOnOrderBy [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName, E.asc $ user E.^. UserEmail ] $ do - E.on $ tutor E.^. TutorUser E.==. user E.^. UserId - E.on $ tutor E.^. TutorTutorial E.==. tutorial E.^. TutorialId - E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid - E.orderBy [ E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName ] - return ( user E.^. UserEmail, user E.^. UserDisplayName, user E.^. UserSurname ) - return (cid,course,schoolName,participants,registration,entityKey <$> defSFid,lecturers,assistants,correctors,tutors) - - mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom course - mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course - mDereg <- traverse (formatTime SelFormatDateTime) $ courseDeregisterUntil course - mRegAt <- traverse (formatTime SelFormatDateTime) $ courseParticipantRegistration <$> registration - (regWidget, regEnctype) <- generateFormPost $ courseRegisterForm mbAid registration defSFid $ courseRegisterSecret course - let regForm = wrapForm regWidget def - { formAction = Just . SomeRoute $ CourseR tid ssh csh CRegisterR - , formEncoding = regEnctype - , formSubmit = FormNoSubmit - } - registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid ssh csh CRegisterR) True - - let - tutorialDBTable = DBTable{..} - where - dbtSQLQuery tutorial = do - E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid - return tutorial - dbtRowKey = (E.^. TutorialId) - dbtProj = return - dbtColonnade = dbColonnade $ mconcat - [ sortable (Just "type") (i18nCell MsgTutorialType) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> textCell $ CI.original tutorialType - , sortable (Just "name") (i18nCell MsgTutorialName) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> indicatorCell <> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) [whamlet|#{tutorialName}|] - , sortable Nothing (i18nCell MsgTutorialTutors) $ \DBRow{ dbrOutput = Entity tutid _ } -> sqlCell $ do - tutTutors <- fmap (map $(unValueN 3)) . E.select . E.from $ \(tutor `E.InnerJoin` user) -> do - E.on $ tutor E.^. TutorUser E.==. user E.^. UserId - E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid - return (user E.^. UserEmail, user E.^. UserDisplayName, user E.^. UserSurname) - return [whamlet| - $newline never -