517 lines
22 KiB
Haskell
517 lines
22 KiB
Haskell
{-# LANGUAGE NoImplicitPrelude
|
|
, RecordWildCards
|
|
, TemplateHaskell
|
|
, OverloadedStrings
|
|
, FlexibleContexts
|
|
, ViewPatterns
|
|
, TypeFamilies
|
|
, DeriveGeneric
|
|
, DeriveDataTypeable
|
|
, QuasiQuotes
|
|
, NamedFieldPuns
|
|
, MultiWayIf
|
|
, NumDecimals
|
|
#-}
|
|
|
|
module Jobs
|
|
( module Types
|
|
, writeJobCtl
|
|
, queueJob, queueJob'
|
|
, handleJobs
|
|
) where
|
|
|
|
import Import hiding ((.=), Proxy)
|
|
import Handler.Utils.Mail
|
|
import Handler.Utils.DateTime
|
|
|
|
import Jobs.Types as Types hiding (JobCtl(JobCtlQueue))
|
|
import Jobs.Types (JobCtl(JobCtlQueue))
|
|
|
|
import Data.Conduit.TMChan
|
|
import qualified Data.Conduit.List as C
|
|
|
|
import qualified Data.Text.Lazy as LT
|
|
|
|
import Data.Aeson (fromJSON, toJSON)
|
|
import qualified Data.Aeson as Aeson
|
|
import qualified Data.Aeson.Types as Aeson
|
|
import Database.Persist.Sql (executeQQ, fromSqlKey, transactionSave)
|
|
|
|
import Data.Monoid (Last(..))
|
|
import Data.Semigroup (Max(..))
|
|
|
|
import Utils.Lens
|
|
|
|
import Control.Monad.Random (evalRand, uniform, mkStdGen)
|
|
|
|
import qualified Database.Esqueleto as E
|
|
|
|
import qualified Data.CaseInsensitive as CI
|
|
|
|
import Text.Shakespeare.Text
|
|
import Text.Hamlet
|
|
|
|
import Cron
|
|
import qualified Data.HashMap.Strict as HashMap
|
|
import Data.HashMap.Strict (HashMap)
|
|
|
|
import Data.List.NonEmpty (NonEmpty, nonEmpty)
|
|
import qualified Data.List.NonEmpty as NonEmpty
|
|
|
|
import Data.Foldable (foldrM)
|
|
|
|
import Control.Monad.Trans.Reader (mapReaderT)
|
|
import Control.Monad.Trans.Writer (WriterT, execWriterT)
|
|
import Control.Monad.Trans.State (StateT, evalStateT, mapStateT)
|
|
import qualified Control.Monad.State.Class as State
|
|
import Control.Monad.Writer.Class (MonadWriter(..))
|
|
import Control.Monad.Reader.Class (MonadReader(..))
|
|
import Control.Monad.Trans.Resource (MonadResourceBase, ResourceT, runResourceT, allocate)
|
|
import Control.Monad.Trans.Maybe (MaybeT(..))
|
|
import Control.Monad.Logger
|
|
|
|
import Control.Monad.Random (MonadRandom(..), evalRand)
|
|
|
|
import Data.Time.Clock
|
|
import Data.Time.Zones
|
|
|
|
import Control.Concurrent.STM (retry)
|
|
|
|
import Database.PostgreSQL.Simple (sqlErrorHint)
|
|
import Control.Monad.Catch (handleIf)
|
|
|
|
|
|
data JobQueueException = JInvalid QueuedJobId QueuedJob
|
|
| JLocked QueuedJobId InstanceId UTCTime
|
|
| JNonexistant QueuedJobId
|
|
deriving (Read, Show, Eq, Generic, Typeable)
|
|
|
|
instance Exception JobQueueException
|
|
|
|
|
|
handleJobs :: (MonadResource m, MonadIO m) => [TMChan JobCtl] -> UniWorX -> m ()
|
|
-- | Read control commands from `appJobCtl` and address them as they come in
|
|
--
|
|
-- Uses `unsafeHandler`, as per documentation all HTTP-related fields of state/environment are meaningless placeholders.
|
|
-- Handling commands in `HandlerT` provides us with the facilities to render urls, unifies logging, provides a value of the foundation type, ...
|
|
handleJobs recvChans foundation@UniWorX{..} = do
|
|
jobCrontab <- liftIO $ newTVarIO HashMap.empty
|
|
jobConfirm <- liftIO $ newTVarIO HashMap.empty
|
|
|
|
forM_ (zip [1..] recvChans) $ \(n, chan) ->
|
|
let
|
|
logStart = $logDebugS ("Jobs #" <> tshow n) "Starting"
|
|
logStop = $logDebugS ("Jobs #" <> tshow n) "Stopping"
|
|
doFork = fork . unsafeHandler foundation . bracket_ logStart logStop . flip runReaderT JobContext{..} . runConduit $ sourceTMChan chan .| handleJobs' n
|
|
in void $ allocate (liftIO doFork) (liftIO . killThread)
|
|
|
|
-- Start cron operation
|
|
void $ allocate (liftIO . fork . unsafeHandler foundation $ runReaderT execCrontab JobContext{..}) (liftIO . killThread)
|
|
liftIO . unsafeHandler foundation . flip runReaderT JobContext{..} $
|
|
writeJobCtlBlock JobCtlDetermineCrontab
|
|
|
|
|
|
execCrontab :: ReaderT JobContext (HandlerT UniWorX IO) ()
|
|
-- ^ Keeping a `HashMap` of the latest execution times of `JobCtl`s we have
|
|
-- seen, wait for the time of the next job and fire it
|
|
execCrontab = flip evalStateT HashMap.empty . forever $ do
|
|
mapStateT (liftHandlerT . runDB . setSerializable) $ do
|
|
let
|
|
merge (Entity leId CronLastExec{..})
|
|
| Just job <- Aeson.parseMaybe parseJSON cronLastExecJob
|
|
= State.modify $ HashMap.insertWith (<>) (JobCtlQueue job) (Max cronLastExecTime)
|
|
| otherwise = lift $ delete leId
|
|
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ merge
|
|
|
|
now <- liftIO getCurrentTime
|
|
(currentCrontab, (jobCtl, nextMatch)) <- mapStateT (mapReaderT $ liftIO . atomically) $ do
|
|
crontab <- liftBase . readTVar =<< asks jobCrontab
|
|
State.modify . HashMap.filterWithKey $ \k _ -> HashMap.member k crontab
|
|
prevExec <- State.get
|
|
case earliestJob prevExec crontab now of
|
|
Nothing -> liftBase retry
|
|
Just (_, MatchNone) -> liftBase retry
|
|
Just x -> return (crontab, x)
|
|
|
|
let doJob = do
|
|
mJid <- mapStateT (mapReaderT $ liftHandlerT . runDB . setSerializable) $ do
|
|
newCrontab <- lift . lift $ determineCrontab
|
|
if
|
|
| ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab
|
|
-> do
|
|
now <- liftIO $ getCurrentTime
|
|
instanceID <- getsYesod appInstanceID
|
|
State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl
|
|
case jobCtl of
|
|
JobCtlQueue job -> do
|
|
lift . lift $ upsertBy
|
|
(UniqueCronLastExec $ toJSON job)
|
|
CronLastExec
|
|
{ cronLastExecJob = toJSON job
|
|
, cronLastExecTime = now
|
|
, cronLastExecInstance = instanceID
|
|
}
|
|
[ CronLastExecTime =. now ]
|
|
Just <$> lift (lift $ queueJobUnsafe job)
|
|
other -> Nothing <$ writeJobCtl other
|
|
| otherwise
|
|
-> lift . fmap (const Nothing) . mapReaderT (liftIO . atomically) $
|
|
lift . flip writeTVar newCrontab =<< asks jobCrontab
|
|
maybe (return ()) (writeJobCtl . JobCtlPerform) mJid
|
|
|
|
case nextMatch of
|
|
MatchAsap -> doJob
|
|
MatchNone -> return ()
|
|
MatchAt nextTime -> do
|
|
JobContext{jobCrontab} <- ask
|
|
nextTime' <- applyJitter jobCtl nextTime
|
|
$logDebugS "Cron" [st|Waiting until #{tshow (utcToLocalTimeTZ appTZ nextTime')} to execute #{tshow jobCtl}|]
|
|
logFunc <- askLoggerIO
|
|
whenM (liftIO . flip runLoggingT logFunc $ waitUntil jobCrontab currentCrontab nextTime')
|
|
doJob
|
|
where
|
|
acc :: NominalDiffTime
|
|
acc = 1e-3
|
|
|
|
applyJitter :: (MonadHandler m, HandlerSite m ~ UniWorX, Hashable seed) => seed -> UTCTime -> m UTCTime
|
|
applyJitter seed t = do
|
|
appInstance <- getsYesod appInstanceID
|
|
let
|
|
halfRange = truncate $ 0.5 / acc
|
|
diff = evalRand ( (* acc) . fromInteger <$> getRandomR (- halfRange, halfRange)) $ mkStdGen (hash appInstance `hashWithSalt` seed)
|
|
return $ addUTCTime diff t
|
|
|
|
earliestJob :: HashMap JobCtl (Max UTCTime) -> Crontab JobCtl -> UTCTime -> Maybe (JobCtl, CronNextMatch UTCTime)
|
|
earliestJob lastTimes crontab now = foldr go Nothing $ HashMap.toList crontab
|
|
where
|
|
go (jobCtl, cron) mbPrev
|
|
| Just (_, t') <- mbPrev
|
|
, t' < t
|
|
= mbPrev
|
|
| otherwise
|
|
= Just (jobCtl, t)
|
|
where
|
|
t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) now cron
|
|
|
|
waitUntil :: (Eq a, MonadResourceBase m, MonadLogger m) => TVar a -> a -> UTCTime -> m Bool
|
|
waitUntil crontabTV crontab nextTime = runResourceT $ do
|
|
diffT <- diffUTCTime nextTime <$> liftIO getCurrentTime
|
|
let waitTime = fromInteger (truncate $ diffT / acc) * toRational acc
|
|
waitTime'
|
|
| diffT < acc = "Done"
|
|
| otherwise = tshow (realToFrac waitTime :: NominalDiffTime)
|
|
$logDebugS "waitUntil" [st|#{tshow diffT} (#{waitTime'})|]
|
|
if
|
|
| diffT < acc -> return True
|
|
| otherwise -> do
|
|
retVar <- liftIO newEmptyTMVarIO
|
|
void $ allocate (liftIO $ forkFinally (threadDelay . round $ waitTime * 1e6) $ atomically . putTMVar retVar) (liftIO . killThread)
|
|
let
|
|
awaitDelayThread = False <$ takeTMVar retVar
|
|
awaitCrontabChange = do
|
|
crontab' <- readTVar crontabTV
|
|
True <$ guard (crontab /= crontab')
|
|
crontabChanged <- liftIO . atomically $ awaitCrontabChange <|> awaitDelayThread
|
|
bool (waitUntil crontabTV crontab nextTime) (return False) crontabChanged
|
|
|
|
|
|
handleJobs' :: Int -> Sink JobCtl (ReaderT JobContext Handler) ()
|
|
handleJobs' wNum = C.mapM_ $ \jctl -> do
|
|
$logDebugS logIdent $ tshow jctl
|
|
resVars <- mapReaderT (liftIO . atomically) $
|
|
HashMap.lookup jctl <$> (lift . readTVar =<< asks jobConfirm)
|
|
res <- fmap (either Just $ const Nothing) . try $ handleCmd jctl
|
|
sentRes <- liftIO . atomically $ foldrM (\resVar -> bool (tryPutTMVar resVar res) $ return True) False (maybe [] NonEmpty.toList resVars)
|
|
case res of
|
|
Just err
|
|
| not sentRes -> $logErrorS logIdent $ tshow err
|
|
_other -> return ()
|
|
where
|
|
logIdent = "Jobs #" <> tshow wNum
|
|
|
|
handleQueueException :: MonadLogger m => JobQueueException -> m ()
|
|
handleQueueException (JInvalid jId j) = $logWarnS logIdent $ "Invalid QueuedJob (#" ++ tshow (fromSqlKey jId) ++ "): " ++ tshow j
|
|
handleQueueException (JNonexistant jId) = $logInfoS logIdent $ "Saw nonexistant queue id: " ++ tshow (fromSqlKey jId)
|
|
handleQueueException (JLocked jId lInstance lTime) = $logDebugS logIdent $ "Saw locked QueuedJob: " ++ tshow (fromSqlKey jId, lInstance, lTime)
|
|
|
|
handleCmd JobCtlFlush = void . lift . runDB . runConduit $ selectKeys [] [ Asc QueuedJobCreationTime ] .| C.mapM_ (writeJobCtl . JobCtlPerform)
|
|
handleCmd (JobCtlQueue job) = lift $ queueJob' job
|
|
handleCmd (JobCtlPerform jId) = lift . handle handleQueueException . jLocked jId $ \j@QueuedJob{..} -> do
|
|
content <- case fromJSON queuedJobContent of
|
|
Aeson.Success c -> return c
|
|
Aeson.Error t -> do
|
|
$logErrorS logIdent $ "Aeson decoding error: " <> pack t
|
|
throwM $ JInvalid jId j
|
|
|
|
$logDebugS logIdent . LT.toStrict . decodeUtf8 $ Aeson.encode content
|
|
|
|
performJob content
|
|
|
|
-- `performJob` is expected to throw an exception if it detects that the job was not done
|
|
runDB $ delete jId
|
|
handleCmd JobCtlDetermineCrontab = do
|
|
newCTab <- liftHandlerT . runDB $ setSerializable determineCrontab
|
|
-- $logDebugS logIdent $ tshow newCTab
|
|
mapReaderT (liftIO . atomically) $
|
|
lift . flip writeTVar newCTab =<< asks jobCrontab
|
|
|
|
jLocked :: QueuedJobId -> (QueuedJob -> Handler a) -> Handler a
|
|
jLocked jId act = do
|
|
hasLock <- liftIO $ newTVarIO False
|
|
|
|
let
|
|
lock = runDB . setSerializable $ do
|
|
qj@QueuedJob{..} <- maybe (throwM $ JNonexistant jId) return =<< get jId
|
|
instanceID <- getsYesod appInstanceID
|
|
threshold <- getsYesod $ appJobStaleThreshold . appSettings
|
|
now <- liftIO getCurrentTime
|
|
hadStale <- maybeT (return False) $ do
|
|
lockTime <- MaybeT $ return queuedJobLockTime
|
|
lockInstance <- MaybeT $ return queuedJobLockInstance
|
|
if
|
|
| lockInstance == instanceID
|
|
, diffUTCTime now lockTime >= threshold
|
|
-> return True
|
|
| otherwise
|
|
-> throwM $ JLocked jId lockInstance lockTime
|
|
when hadStale .
|
|
$logWarnS "Jobs" $ "Ignored stale lock: " <> tshow qj
|
|
val <- updateGet jId [ QueuedJobLockInstance =. Just instanceID
|
|
, QueuedJobLockTime =. Just now
|
|
]
|
|
liftIO . atomically $ writeTVar hasLock True
|
|
return val
|
|
|
|
unlock = whenM (liftIO . atomically $ readTVar hasLock) $
|
|
runDB . setSerializable $
|
|
update jId [ QueuedJobLockInstance =. Nothing
|
|
, QueuedJobLockTime =. Nothing
|
|
]
|
|
|
|
bracket lock (const unlock) act
|
|
|
|
writeJobCtl :: (MonadHandler m, HandlerSite m ~ UniWorX) => JobCtl -> m ()
|
|
writeJobCtl cmd = do
|
|
tid <- liftIO myThreadId
|
|
chan <- flip evalRand (mkStdGen (hash tid `hashWithSalt` cmd)) . uniform <$> getsYesod appJobCtl
|
|
liftIO . atomically $ writeTMChan chan cmd
|
|
|
|
writeJobCtlBlock :: (MonadHandler m, HandlerSite m ~ UniWorX) => JobCtl -> ReaderT JobContext m ()
|
|
writeJobCtlBlock cmd = do
|
|
getResVar <- asks jobConfirm
|
|
resVar <- liftIO . atomically $ do
|
|
var <- newEmptyTMVar
|
|
modifyTVar' getResVar $ HashMap.insertWith (<>) cmd (pure var)
|
|
return var
|
|
lift $ writeJobCtl cmd
|
|
let
|
|
removeResVar = HashMap.update (nonEmpty . NonEmpty.filter (/= resVar)) cmd
|
|
mExc <- liftIO . atomically $ takeTMVar resVar <* modifyTVar' getResVar removeResVar
|
|
maybe (return ()) throwM mExc
|
|
|
|
queueJobUnsafe :: Job -> YesodDB UniWorX QueuedJobId
|
|
queueJobUnsafe job = do
|
|
now <- liftIO getCurrentTime
|
|
self <- getsYesod appInstanceID
|
|
insert QueuedJob
|
|
{ queuedJobContent = toJSON job
|
|
, queuedJobCreationInstance = self
|
|
, queuedJobCreationTime = now
|
|
, queuedJobLockInstance = Nothing
|
|
, queuedJobLockTime = Nothing
|
|
}
|
|
-- We should not immediately notify a worker; instead wait for the transaction to finish first
|
|
-- writeJobCtl $ JobCtlPerform jId -- FIXME: Should do fancy load balancing across instances (or something)
|
|
-- return jId
|
|
|
|
queueJob :: (MonadHandler m, HandlerSite m ~ UniWorX) => Job -> m QueuedJobId
|
|
queueJob = liftHandlerT . runDB . setSerializable . queueJobUnsafe
|
|
|
|
queueJob' :: (MonadHandler m, HandlerSite m ~ UniWorX) => Job -> m ()
|
|
-- ^ `queueJob` followed by `JobCtlPerform`
|
|
queueJob' job = queueJob job >>= writeJobCtl . JobCtlPerform
|
|
|
|
setSerializable :: DB a -> DB a
|
|
setSerializable act = setSerializable' (0 :: Integer)
|
|
where
|
|
act' = [executeQQ|SET TRANSACTION ISOLATION LEVEL SERIALIZABLE|] *> act
|
|
|
|
setSerializable' (min 10 -> logBackoff) =
|
|
handleIf
|
|
(\e -> sqlErrorHint e == "The transaction might succeed if retried.")
|
|
(\e -> $logWarnS "SQL" (tshow e) *> threadDelay (1e3 * 2 ^ logBackoff) *> setSerializable' (succ logBackoff))
|
|
act'
|
|
|
|
pruneLastExecs :: Crontab JobCtl -> DB ()
|
|
pruneLastExecs crontab = runConduit $ selectSource [] [] .| C.mapM_ ensureCrontab
|
|
where
|
|
ensureCrontab (Entity leId CronLastExec{..})
|
|
| Just job <- Aeson.parseMaybe parseJSON cronLastExecJob
|
|
, HashMap.member (JobCtlQueue job) crontab
|
|
= return ()
|
|
| otherwise = delete leId
|
|
|
|
|
|
determineCrontab :: DB (Crontab JobCtl)
|
|
-- ^ Extract all future jobs from the database (sheet deadlines, ...)
|
|
determineCrontab = (\ct -> ct <$ pruneLastExecs ct) <=< execWriterT $ do
|
|
AppSettings{..} <- getsYesod appSettings
|
|
|
|
case appJobFlushInterval of
|
|
Just interval -> tell $ HashMap.singleton
|
|
JobCtlFlush
|
|
Cron
|
|
{ cronInitial = CronAsap
|
|
, cronRepeat = CronRepeatScheduled CronAsap
|
|
, cronRateLimit = interval
|
|
}
|
|
Nothing -> return ()
|
|
|
|
now <- liftIO getCurrentTime
|
|
tell $ HashMap.singleton
|
|
JobCtlDetermineCrontab
|
|
Cron
|
|
{ cronInitial = CronAsap
|
|
, cronRepeat = CronRepeatScheduled CronAsap
|
|
, cronRateLimit = appJobCronInterval
|
|
}
|
|
|
|
let
|
|
sheetJobs (Entity nSheet Sheet{..}) = do
|
|
tell $ HashMap.singleton
|
|
(JobCtlQueue $ JobQueueNotification NotificationSheetActive{..})
|
|
Cron
|
|
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ sheetActiveFrom
|
|
, cronRepeat = CronRepeatOnChange -- Allow repetition of the notification (if something changes), but wait at least an hour
|
|
, cronRateLimit = appNotificationRateLimit
|
|
}
|
|
tell $ HashMap.singleton
|
|
(JobCtlQueue $ JobQueueNotification NotificationSheetInactive{..})
|
|
Cron
|
|
{ cronInitial = CronTimestamp . utcToLocalTimeTZ appTZ . max sheetActiveFrom $ addUTCTime (-nominalDay) sheetActiveTo
|
|
, cronRepeat = CronRepeatOnChange
|
|
, cronRateLimit = appNotificationRateLimit
|
|
}
|
|
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ sheetJobs
|
|
|
|
|
|
determineNotificationCandidates :: Notification -> DB [Entity User]
|
|
determineNotificationCandidates NotificationSubmissionRated{..} = E.select . E.from $ \(user `E.InnerJoin` submissionUser) -> do
|
|
E.on $ user E.^. UserId E.==. submissionUser E.^. SubmissionUserUser
|
|
E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val nSubmission
|
|
return user
|
|
determineNotificationCandidates NotificationSheetActive{..} = E.select . E.from $ \(user `E.InnerJoin` courseParticipant `E.InnerJoin` sheet) -> do
|
|
E.on $ sheet E.^. SheetCourse E.==. courseParticipant E.^. CourseParticipantCourse
|
|
E.on $ user E.^. UserId E.==. courseParticipant E.^. CourseParticipantUser
|
|
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
|
|
return user
|
|
determineNotificationCandidates NotificationSheetInactive{..} = E.select . E.from $ \(user `E.InnerJoin` courseParticipant `E.InnerJoin` sheet) -> do
|
|
E.on $ sheet E.^. SheetCourse E.==. courseParticipant E.^. CourseParticipantCourse
|
|
E.on $ user E.^. UserId E.==. courseParticipant E.^. CourseParticipantUser
|
|
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
|
|
return user
|
|
|
|
classifyNotification :: Notification -> DB NotificationTrigger
|
|
classifyNotification NotificationSubmissionRated{..} = do
|
|
Sheet{sheetType} <- belongsToJust submissionSheet =<< getJust nSubmission
|
|
return $ case sheetType of
|
|
NotGraded -> NTSubmissionRated
|
|
_other -> NTSubmissionRatedGraded
|
|
classifyNotification NotificationSheetActive{} = return NTSheetActive
|
|
classifyNotification NotificationSheetInactive{} = return NTSheetInactive
|
|
|
|
|
|
performJob :: Job -> HandlerT UniWorX IO ()
|
|
performJob JobQueueNotification{jNotification} = do
|
|
jIds <- runDB. setSerializable $ do
|
|
candidates <- determineNotificationCandidates jNotification
|
|
nClass <- classifyNotification jNotification
|
|
mapM (queueJobUnsafe . flip JobSendNotification jNotification) $ do
|
|
Entity uid User{userNotificationSettings} <- candidates
|
|
guard $ notificationAllowed userNotificationSettings nClass
|
|
return uid
|
|
forM_ jIds $ writeJobCtl . JobCtlPerform
|
|
performJob JobSendNotification{ jNotification = NotificationSubmissionRated{..}, jRecipient } = userMailT jRecipient $ do
|
|
(Course{..}, Sheet{..}, Submission{..}, corrector) <- liftHandlerT . runDB $ do
|
|
submission@Submission{submissionRatingBy} <- getJust nSubmission
|
|
sheet <- belongsToJust submissionSheet submission
|
|
course <- belongsToJust sheetCourse sheet
|
|
corrector <- traverse getJust submissionRatingBy
|
|
return (course, sheet, submission, corrector)
|
|
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
|
|
|
|
csid <- encrypt nSubmission
|
|
MsgRenderer mr <- getMailMsgRenderer
|
|
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
|
|
submissionRatingTime' <- traverse (formatTimeMail SelFormatDateTime) submissionRatingTime
|
|
let tid = courseTerm
|
|
ssh = courseSchool
|
|
csh = courseShorthand
|
|
shn = sheetName
|
|
|
|
-- TODO: provide convienience template-haskell for `addAlternatives`
|
|
addAlternatives $ do
|
|
provideAlternative $ Aeson.object
|
|
[ "submission" Aeson..= ciphertext csid
|
|
, "submission-rating-points" Aeson..= submissionRatingPoints
|
|
, "submission-rating-comment" Aeson..= submissionRatingComment
|
|
, "submission-rating-time" Aeson..= submissionRatingTime
|
|
, "submission-rating-by" Aeson..= (userDisplayName <$> corrector)
|
|
, "submission-rating-passed" Aeson..= ((>=) <$> submissionRatingPoints <*> preview _passingPoints sheetType)
|
|
, "sheet-name" Aeson..= sheetName
|
|
, "sheet-type" Aeson..= sheetType
|
|
, "course-name" Aeson..= courseName
|
|
, "course-shorthand" Aeson..= courseShorthand
|
|
, "course-term" Aeson..= courseTerm
|
|
, "course-school" Aeson..= courseSchool
|
|
]
|
|
-- provideAlternative $ \(MsgRenderer mr) -> ($(textFile "templates/mail/submissionRated.txt") :: TextUrl (Route UniWorX)) -- textFile does not support control statements
|
|
providePreferredAlternative ($(ihamletFile "templates/mail/submissionRated.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
|
performJob JobSendNotification{ jNotification = NotificationSheetActive{..}, jRecipient } = userMailT jRecipient $ do
|
|
(Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do
|
|
sheet <- getJust nSheet
|
|
course <- belongsToJust sheetCourse sheet
|
|
return (course, sheet)
|
|
setSubjectI $ MsgMailSubjectSheetActive courseShorthand sheetName
|
|
|
|
MsgRenderer mr <- getMailMsgRenderer
|
|
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
|
|
tid = courseTerm
|
|
ssh = courseSchool
|
|
csh = courseShorthand
|
|
shn = sheetName
|
|
|
|
addAlternatives $ do
|
|
providePreferredAlternative ($(ihamletFile "templates/mail/sheetActive.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
|
performJob JobSendNotification{ jNotification = NotificationSheetInactive{..}, jRecipient } = userMailT jRecipient $ do
|
|
(Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do
|
|
sheet <- getJust nSheet
|
|
course <- belongsToJust sheetCourse sheet
|
|
return (course, sheet)
|
|
setSubjectI $ MsgMailSubjectSheetInactive courseShorthand sheetName
|
|
|
|
MsgRenderer mr <- getMailMsgRenderer
|
|
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
|
|
tid = courseTerm
|
|
ssh = courseSchool
|
|
csh = courseShorthand
|
|
shn = sheetName
|
|
|
|
addAlternatives $ do
|
|
providePreferredAlternative ($(ihamletFile "templates/mail/sheetInactive.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
|
performJob JobSendTestEmail{..} = mailT jMailContext $ do
|
|
_mailTo .= [Address Nothing jEmail]
|
|
setSubjectI MsgMailTestSubject
|
|
now <- liftIO getCurrentTime
|
|
nDT <- formatTimeMail SelFormatDateTime now
|
|
nD <- formatTimeMail SelFormatDate now
|
|
nT <- formatTimeMail SelFormatTime now
|
|
addPart $ \(MsgRenderer mr) -> ([text|
|
|
#{mr MsgMailTestContent}
|
|
|
|
#{mr MsgMailTestDateTime}
|
|
* #{nDT}
|
|
* #{nD}
|
|
* #{nT}
|
|
|] :: TextUrl (Route UniWorX))
|