136 lines
6.0 KiB
Haskell
136 lines
6.0 KiB
Haskell
{-# OPTIONS_GHC -fno-warn-redundant-constraints -fno-warn-incomplete-uni-patterns #-}
|
|
module Handler.Participants
|
|
( getParticipantsListR
|
|
, getParticipantsR
|
|
, getParticipantsIntersectR, postParticipantsIntersectR
|
|
) where
|
|
|
|
import Import
|
|
import Handler.Utils
|
|
|
|
import qualified Database.Esqueleto as E
|
|
import qualified Database.Esqueleto.Utils as E
|
|
|
|
import qualified Data.Map as Map
|
|
import qualified Data.Set as Set
|
|
import Data.Map ((!), (!?))
|
|
|
|
import Handler.Utils.Csv
|
|
|
|
import qualified Data.Csv as Csv
|
|
|
|
import qualified Data.Conduit.List as C
|
|
|
|
|
|
data ParticipantEntry = ParticipantEntry
|
|
{ peCourse :: CourseName
|
|
, peEmail :: UserEmail
|
|
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
|
|
|
instance ToNamedRecord ParticipantEntry where
|
|
toNamedRecord ParticipantEntry{..} = Csv.namedRecord
|
|
[ "course" Csv..= peCourse
|
|
, "email" Csv..= peEmail
|
|
]
|
|
|
|
instance DefaultOrdered ParticipantEntry where
|
|
headerOrder _ = Csv.header ["course", "email"]
|
|
|
|
|
|
getParticipantsListR :: Handler Html
|
|
getParticipantsListR = do
|
|
schoolTerms'' <- runDB . E.select . E.from $ \(school `E.InnerJoin` term) -> do
|
|
E.on E.true
|
|
|
|
E.where_ . E.exists . E.from $ \(course `E.InnerJoin` participant) -> do
|
|
E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
|
|
E.where_ $ course E.^. CourseTerm E.==. term E.^. TermId
|
|
E.&&. course E.^. CourseSchool E.==. school E.^. SchoolId
|
|
E.where_ $ participant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
|
|
|
|
return (school E.^. SchoolId, term E.^. TermId)
|
|
|
|
schoolTerms' <- flip filterM schoolTerms'' $ \(E.Value ssh, E.Value tid) ->
|
|
hasReadAccessTo $ ParticipantsR tid ssh
|
|
|
|
let schoolTerms :: Set (SchoolId, TermId)
|
|
schoolTerms = setOf (folded . $(multifocusG 2) (_1 . _Value) (_2 . _Value)) schoolTerms'
|
|
|
|
siteLayoutMsg MsgMenuParticipantsList $ do
|
|
setTitleI MsgMenuParticipantsList
|
|
|
|
let schools :: Set SchoolId
|
|
schools = Set.map (view _1) schoolTerms
|
|
terms :: Set TermId
|
|
terms = Set.map (view _2) schoolTerms
|
|
$(widgetFile "participants-list")
|
|
|
|
getParticipantsR :: TermId -> SchoolId -> Handler TypedContent
|
|
getParticipantsR tid ssh = do
|
|
csvName <- timestampCsv <*> fmap ((flip addExtension `on` unpack) extensionCsv) (getMessageRender <*> pure (MsgParticipantsCsvName tid ssh))
|
|
setContentDisposition' $ Just csvName
|
|
respondDefaultOrderedCsvDB $ E.selectSource partQuery .| C.map toParticipantEntry
|
|
where
|
|
partQuery = E.from $ \(course `E.InnerJoin` participant `E.InnerJoin` user) -> do
|
|
E.on $ user E.^. UserId E.==. participant E.^. CourseParticipantUser
|
|
E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
|
|
|
|
E.where_ $ course E.^. CourseTerm E.==. E.val tid
|
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
|
E.where_ $ participant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
|
|
|
|
return (course E.^. CourseName, user E.^. UserEmail)
|
|
|
|
toParticipantEntry (E.Value peCourse, E.Value peEmail) = ParticipantEntry{..}
|
|
|
|
getParticipantsIntersectR, postParticipantsIntersectR :: Handler Html
|
|
getParticipantsIntersectR = postParticipantsIntersectR
|
|
postParticipantsIntersectR = do
|
|
let
|
|
courseQuery = E.from return
|
|
termSchoolAccess (Entity _ Course{..}) =
|
|
hasReadAccessTo $ ParticipantsR courseTerm courseSchool
|
|
((coursesRes, coursesView), coursesEnc) <- runFormPost . renderAForm FormStandard $ courseSelectForm courseQuery termSchoolAccess (const Nothing) ("participants-intersect" :: Text) (fslI MsgParticipantsIntersectCourses) False Nothing
|
|
let formWidget = wrapForm coursesView def
|
|
{ formAction = Just . SomeRoute $ ParticipantsIntersectR :#: ("table" :: Text)
|
|
, formEncoding = coursesEnc
|
|
}
|
|
|
|
intersectionsRes <- formResultMaybe coursesRes . fmap (fmap Just) $ \cids -> runDB $ do
|
|
let coursePairs = do
|
|
cid <- Set.toList cids
|
|
other <- Set.toList . snd $ Set.split cid cids
|
|
return (cid, other)
|
|
intersections <- fmap Map.fromList . forM coursePairs $ \cidPair@(lCid, uCid) -> fmap (\[E.Value n] -> (cidPair, n)) . E.select . E.from $ \user -> do
|
|
E.where_ . E.exists . E.from $ \courseParticipant ->
|
|
E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId
|
|
E.&&. courseParticipant E.^. CourseParticipantCourse E.==. E.val lCid
|
|
E.&&. courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
|
|
E.where_ . E.exists . E.from $ \courseParticipant ->
|
|
E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId
|
|
E.&&. courseParticipant E.^. CourseParticipantCourse E.==. E.val uCid
|
|
E.&&. courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
|
|
return E.countRows :: E.SqlQuery (E.SqlExpr (E.Value Int))
|
|
selfIntersections <- fmap Map.fromList . forM (Set.toList cids) $ \cid -> ((cid, cid), ) <$> count [CourseParticipantCourse ==. cid, CourseParticipantState ==. CourseParticipantActive]
|
|
let intersections' = Map.union intersections selfIntersections
|
|
courses <- sortOn (view $ _entityVal . $(multifocusL 3) _courseTerm _courseSchool _courseShorthand) <$> forM (Set.toList cids) getEntity404
|
|
return (courses, intersections')
|
|
|
|
let
|
|
symmIntersection intersections lCid uCid = fromMaybe 0 $ intersections !? (lCid, uCid) <|> intersections !? (uCid, lCid)
|
|
intersectionHotness :: _ -> _ -> _ -> Centi
|
|
intersectionHotness intersections lCid uCid
|
|
| sumSize == 0 = 0
|
|
| intersectSize == 0 = 0
|
|
| otherwise = realToFrac . (0.5 +) . (0.5 *) . max 0 . min 1 $ 2 * intersectSize % sumSize
|
|
where
|
|
sumSize = (min `on` (intersections !)) (lCid, lCid) (uCid, uCid)
|
|
intersectSize = symmIntersection intersections lCid uCid
|
|
showNumber n lCid uCid = n /= 0 || lCid == uCid
|
|
|
|
lIxed = zip [0..]
|
|
|
|
siteLayoutMsg MsgMenuParticipantsIntersect $ do
|
|
setTitleI MsgMenuParticipantsIntersect
|
|
$(widgetFile "participants-intersect")
|