Merge branch 'master' of gitlab.cip.ifi.lmu.de:jost/UniWorX

This commit is contained in:
SJost 2018-11-28 09:27:37 +01:00
commit 7ea7b6f0fc
16 changed files with 110 additions and 74 deletions

3
.gitignore vendored
View File

@ -30,4 +30,5 @@ src/Handler/Course.SnapCustom.hs
/instance /instance
.stack-work-* .stack-work-*
.directory .directory
tags tags
test.log

View File

@ -29,9 +29,11 @@ notification-expiration: 259201
session-timeout: 7200 session-timeout: 7200
log-settings: log-settings:
log-detailed: "_env:DETAILED_LOGGING:false" detailed: "_env:DETAILED_LOGGING:false"
log-all: "_env:LOG_ALL:false" all: "_env:LOG_ALL:false"
log-minimum-level: "_env:LOGLEVEL:warn" minimum-level: "_env:LOGLEVEL:warn"
destination: "_env:LOGDEST:stderr"
# Debugging # Debugging
auth-dummy-login: "_env:DUMMY_LOGIN:false" auth-dummy-login: "_env:DUMMY_LOGIN:false"

View File

@ -1,11 +1,10 @@
database: database:
# NOTE: By design, this setting prevents the PGDATABASE environment variable database: "_env:PGDATABASE_TEST:uniworx_test"
# from affecting test runs, so that we don't accidentally affect the
# production database during testing. If you're not concerned about that and log-settings:
# would like to have environment variable overrides, you could instead use detailed: true
# something like: all: true
# minimum-level: "debug"
# database: "_env:PGDATABASE:uniworx_test" destination: "test.log"
database: uniworx_test
auth-dummy-login: true auth-dummy-login: true

View File

@ -30,8 +30,9 @@ import Network.Wai.Middleware.RequestLogger (Destination (Logger),
IPAddrSource (..), IPAddrSource (..),
OutputFormat (..), destination, OutputFormat (..), destination,
mkRequestLogger, outputFormat) mkRequestLogger, outputFormat)
import System.Log.FastLogger (defaultBufSize, newStderrLoggerSet, import System.Log.FastLogger ( defaultBufSize, newStderrLoggerSet, newStdoutLoggerSet, newFileLoggerSet
toLogStr) , toLogStr, rmLoggerSet
)
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
@ -61,7 +62,7 @@ import qualified Yesod.Core.Types as Yesod (Logger(..))
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Control.Lens ((&)) import Control.Lens
import Data.Proxy import Data.Proxy
@ -100,10 +101,30 @@ makeFoundation appSettings@AppSettings{..} = do
-- Some basic initializations: HTTP connection manager, logger, and static -- Some basic initializations: HTTP connection manager, logger, and static
-- subsite. -- subsite.
appHttpManager <- newManager appHttpManager <- newManager
appLogger <- liftIO $ do appLogSettings <- liftIO $ newTVarIO appInitialLogSettings
tgetter <- newTimeCache "%Y-%m-%d %T %z"
loggerSet <- newStderrLoggerSet defaultBufSize let
return $ Yesod.Logger loggerSet tgetter mkLogger LogSettings{..} = do
tgetter <- newTimeCache "%Y-%m-%d %T %z"
loggerSet <- case logDestination of
LogDestStderr -> newStderrLoggerSet defaultBufSize
LogDestStdout -> newStdoutLoggerSet defaultBufSize
LogDestFile{..} -> newFileLoggerSet defaultBufSize logDestFile
return $ Yesod.Logger loggerSet tgetter
mkLogger' = liftIO $ do
initialSettings <- readTVarIO appLogSettings
tVar <- newTVarIO =<< mkLogger initialSettings
let updateLogger prevSettings = do
newSettings <- atomically $ do
newSettings <- readTVar appLogSettings
guard $ newSettings /= prevSettings
return newSettings
oldLogger <- atomically . swapTVar tVar =<< mkLogger newSettings
rmLoggerSet $ loggerSet oldLogger
updateLogger newSettings
(tVar, ) <$> fork (updateLogger initialSettings)
appLogger <- over _2 fst <$> allocate mkLogger' (\(tVar, tId) -> killThread tId >> (readTVarIO tVar >>= rmLoggerSet . loggerSet))
appStatic <- liftIO $ bool static staticDevel appMutableStatic appStaticDir appStatic <- liftIO $ bool static staticDevel appMutableStatic appStaticDir
appInstanceID <- liftIO $ maybe UUID.nextRandom (either readInstanceIDFile return) appInitialInstanceID appInstanceID <- liftIO $ maybe UUID.nextRandom (either readInstanceIDFile return) appInitialInstanceID
@ -111,8 +132,6 @@ makeFoundation appSettings@AppSettings{..} = do
appJobCtl <- liftIO $ newTVarIO Map.empty appJobCtl <- liftIO $ newTVarIO Map.empty
appCronThread <- liftIO newEmptyTMVarIO appCronThread <- liftIO newEmptyTMVarIO
appLogSettings <- liftIO $ newTVarIO appInitialLogSettings
-- We need a log function to create a connection pool. We need a connection -- We need a log function to create a connection pool. We need a connection
-- pool to create our foundation. And we need our foundation to get a -- pool to create our foundation. And we need our foundation to get a
-- logging function. To get out of this loop, we initially create a -- logging function. To get out of this loop, we initially create a
@ -128,7 +147,9 @@ makeFoundation appSettings@AppSettings{..} = do
(error "cryptoIDKey forced in tempFoundation") (error "cryptoIDKey forced in tempFoundation")
(error "sessionKey forced in tempFoundation") (error "sessionKey forced in tempFoundation")
(error "errorMsgKey forced in tempFoundation") (error "errorMsgKey forced in tempFoundation")
logFunc = messageLoggerSource tempFoundation appLogger logFunc loc src lvl str = do
f <- messageLoggerSource tempFoundation <$> readTVarIO (snd appLogger)
f loc src lvl str
flip runLoggingT logFunc $ do flip runLoggingT logFunc $ do
$logDebugS "InstanceID" $ UUID.toText appInstanceID $logDebugS "InstanceID" $ UUID.toText appInstanceID
@ -228,12 +249,13 @@ makeLogWare app = do
let let
mkLogWare ls@LogSettings{..} = do mkLogWare ls@LogSettings{..} = do
logger <- readTVarIO . snd $ appLogger app
logWare <- mkRequestLogger def logWare <- mkRequestLogger def
{ outputFormat = bool { outputFormat = bool
(Apache . bool FromSocket FromHeader . appIpFromHeader $ appSettings app) (Apache . bool FromSocket FromHeader . appIpFromHeader $ appSettings app)
(Detailed True) (Detailed True)
logDetailed logDetailed
, destination = Logger . loggerSet $ appLogger app , destination = Logger $ loggerSet logger
} }
atomically . modifyTVar' logWareMap $ HashMap.insert ls logWare atomically . modifyTVar' logWareMap $ HashMap.insert ls logWare
return logWare return logWare
@ -255,9 +277,11 @@ warpSettings foundation = defaultSettings
& setPort (appPort $ appSettings foundation) & setPort (appPort $ appSettings foundation)
& setHost (appHost $ appSettings foundation) & setHost (appHost $ appSettings foundation)
& setOnException (\_req e -> & setOnException (\_req e ->
when (defaultShouldDisplayException e) $ messageLoggerSource when (defaultShouldDisplayException e) $ do
logger <- readTVarIO . snd $ appLogger foundation
messageLoggerSource
foundation foundation
(appLogger foundation) logger
$(qLocation >>= liftLoc) $(qLocation >>= liftLoc)
"yesod" "yesod"
LevelError LevelError
@ -322,7 +346,9 @@ getApplicationRepl = do
return (getPort wsettings, foundation, app1) return (getPort wsettings, foundation, app1)
shutdownApp :: MonadIO m => UniWorX -> m () shutdownApp :: MonadIO m => UniWorX -> m ()
shutdownApp = stopJobCtl shutdownApp app = do
stopJobCtl app
release . fst $ appLogger app
--------------------------------------------- ---------------------------------------------

View File

@ -150,10 +150,11 @@ genMatch p m st (CronMatchUnion aGen bGen) = merge (genMatch p m st aGen) (genMa
nextCronMatch :: TZ -- ^ Timezone of the `Cron`-Entry nextCronMatch :: TZ -- ^ Timezone of the `Cron`-Entry
-> Maybe UTCTime -- ^ Time of last execution of the job -> Maybe UTCTime -- ^ Time of last execution of the job
-> NominalDiffTime -- ^ Scheduling precision
-> UTCTime -- ^ Current time, used only for `CronCalendar` -> UTCTime -- ^ Current time, used only for `CronCalendar`
-> Cron -> Cron
-> CronNextMatch UTCTime -> CronNextMatch UTCTime
nextCronMatch tz mPrev now c@Cron{..} = case notAfter of nextCronMatch tz mPrev prec now c@Cron{..} = case notAfter of
MatchAsap -> MatchNone MatchAsap -> MatchNone
MatchAt ts MatchAt ts
| MatchAt ts' <- nextMatch | MatchAt ts' <- nextMatch
@ -183,7 +184,7 @@ nextCronMatch tz mPrev now c@Cron{..} = case notAfter of
Just prevT Just prevT
-> case cronRepeat of -> case cronRepeat of
CronRepeatOnChange CronRepeatOnChange
| not $ matchesCron tz Nothing prevT c | not $ matchesCron tz Nothing prec prevT c
-> let -> let
cutoffTime = addUTCTime cronRateLimit prevT cutoffTime = addUTCTime cronRateLimit prevT
in case execRef now False cronInitial of in case execRef now False cronInitial of
@ -240,13 +241,14 @@ nextCronMatch tz mPrev now c@Cron{..} = case notAfter of
matchesCron :: TZ -- ^ Timezone of the `Cron`-Entry matchesCron :: TZ -- ^ Timezone of the `Cron`-Entry
-> Maybe UTCTime -- ^ Previous execution of the job -> Maybe UTCTime -- ^ Previous execution of the job
-> NominalDiffTime -- ^ Scheduling precision
-> UTCTime -- ^ "Current" time -> UTCTime -- ^ "Current" time
-> Cron -> Cron
-> Bool -> Bool
-- ^ @matchesCron tz prev prec now c@ determines whether the given `Cron` -- ^ @matchesCron tz prev prec now c@ determines whether the given `Cron`
-- specification @c@ should match @now@, under the assumption that the next -- specification @c@ should match @now@, under the assumption that the next
-- check will occur no earlier than @now + prec@. -- check will occur no earlier than @now + prec@.
matchesCron tz mPrev now cron = case nextCronMatch tz mPrev now cron of matchesCron tz mPrev prec now cron = case nextCronMatch tz mPrev prec now cron of
MatchAsap -> True MatchAsap -> True
MatchNone -> False MatchNone -> False
MatchAt ts -> ts <= now MatchAt ts -> ts <= addUTCTime prec now

View File

@ -104,7 +104,7 @@ data UniWorX = UniWorX
, appConnPool :: ConnectionPool -- ^ Database connection pool. , appConnPool :: ConnectionPool -- ^ Database connection pool.
, appSmtpPool :: Maybe SMTPPool , appSmtpPool :: Maybe SMTPPool
, appHttpManager :: Manager , appHttpManager :: Manager
, appLogger :: Logger , appLogger :: (ReleaseKey, TVar Logger)
, appLogSettings :: TVar LogSettings , appLogSettings :: TVar LogSettings
, appCryptoIDKey :: CryptoIDKey , appCryptoIDKey :: CryptoIDKey
, appInstanceID :: InstanceId , appInstanceID :: InstanceId
@ -678,10 +678,10 @@ instance Yesod UniWorX where
encrypted plaintextJson plaintext = do encrypted plaintextJson plaintext = do
canDecrypt <- (== Authorized) <$> evalAccess AdminErrMsgR True canDecrypt <- (== Authorized) <$> evalAccess AdminErrMsgR True
shouldEncrypt <- getsYesod $ appEncryptErrors . appSettings shouldEncrypt <- getsYesod $ appEncryptErrors . appSettings
errKey <- getsYesod appErrorMsgKey
if if
| shouldEncrypt | shouldEncrypt
, not canDecrypt -> do , not canDecrypt -> do
errKey <- getsYesod appErrorMsgKey
nonce <- liftIO SecretBox.newNonce nonce <- liftIO SecretBox.newNonce
let ciphertext = SecretBox.secretbox errKey nonce . Lazy.ByteString.toStrict $ encode plaintextJson let ciphertext = SecretBox.secretbox errKey nonce . Lazy.ByteString.toStrict $ encode plaintextJson
encoded = decodeUtf8 . Base64.encode $ Saltine.encode nonce <> ciphertext encoded = decodeUtf8 . Base64.encode $ Saltine.encode nonce <> ciphertext
@ -757,7 +757,7 @@ instance Yesod UniWorX where
LogSettings{..} <- readTVarIO $ appLogSettings app LogSettings{..} <- readTVarIO $ appLogSettings app
return $ logAll || level >= logMinimumLevel return $ logAll || level >= logMinimumLevel
makeLogger = return . appLogger makeLogger = readTVarIO . snd . appLogger
siteLayout :: Maybe Html -- ^ Optionally override `pageHeading` siteLayout :: Maybe Html -- ^ Optionally override `pageHeading`
@ -1694,7 +1694,9 @@ instance HasHttpManager UniWorX where
getHttpManager = appHttpManager getHttpManager = appHttpManager
unsafeHandler :: UniWorX -> Handler a -> IO a unsafeHandler :: UniWorX -> Handler a -> IO a
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger unsafeHandler f h = do
logger <- makeLogger f
Unsafe.fakeHandlerGetLogger (const logger) f h
instance YesodMail UniWorX where instance YesodMail UniWorX where

View File

@ -207,7 +207,7 @@ execCrontab = evalStateT go HashMap.empty
| otherwise | otherwise
= Just (jobCtl, t) = Just (jobCtl, t)
where where
t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) now cron t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) acc now cron
waitUntil :: (Eq a, MonadResourceBase m, MonadLogger m) => TMVar a -> a -> UTCTime -> m Bool waitUntil :: (Eq a, MonadResourceBase m, MonadLogger m) => TMVar a -> a -> UTCTime -> m Bool
waitUntil crontabTV crontab nextTime = runResourceT $ do waitUntil crontabTV crontab nextTime = runResourceT $ do

View File

@ -50,9 +50,9 @@ share [mkPersist sqlSettings, mkMigrate "migrateDBVersioning"]
deriving Show Eq Ord deriving Show Eq Ord
|] |]
migrateAll :: MonadIO m => ReaderT SqlBackend m () migrateAll :: (MonadLogger m, MonadBaseControl IO m, MonadIO m) => ReaderT SqlBackend m ()
migrateAll = do migrateAll = do
runMigration $ do mapM_ ($logInfoS "Migration") <=< runMigrationSilent $ do
-- Manual migrations to go to InitialVersion below: -- Manual migrations to go to InitialVersion below:
migrateEnableExtension "citext" migrateEnableExtension "citext"
@ -69,7 +69,7 @@ migrateAll = do
-- Map.foldlWithKey traverses migrations in ascending order of AppliedMigrationKey -- Map.foldlWithKey traverses migrations in ascending order of AppliedMigrationKey
Map.foldlWithKey doCustomMigration (return ()) missingMigrations Map.foldlWithKey doCustomMigration (return ()) missingMigrations
runMigration migrateAll' mapM_ ($logInfoS "Migration") =<< runMigrationSilent migrateAll'
{- {-
Confusion about quotes, from the PostgreSQL Manual: Confusion about quotes, from the PostgreSQL Manual:

View File

@ -114,11 +114,16 @@ data AppSettings = AppSettings
data LogSettings = LogSettings data LogSettings = LogSettings
{ logAll, logDetailed :: Bool { logAll, logDetailed :: Bool
, logMinimumLevel :: LogLevel , logMinimumLevel :: LogLevel
, logDestination :: LogDestination
} deriving (Show, Read, Generic, Eq, Ord) } deriving (Show, Read, Generic, Eq, Ord)
data LogDestination = LogDestStderr | LogDestStdout | LogDestFile { logDestFile :: !FilePath }
deriving (Show, Read, Generic, Eq, Ord)
deriving instance Generic LogLevel deriving instance Generic LogLevel
instance Hashable LogLevel instance Hashable LogLevel
instance Hashable LogSettings instance Hashable LogSettings
instance Hashable LogDestination
data UserDefaultConf = UserDefaultConf data UserDefaultConf = UserDefaultConf
{ userDefaultTheme :: Theme { userDefaultTheme :: Theme
@ -178,12 +183,19 @@ data SmtpAuthConf = SmtpAuthConf
} deriving (Show) } deriving (Show)
deriveJSON defaultOptions deriveJSON defaultOptions
{ fieldLabelModifier = intercalate "-" . map toLower . splitCamel { constructorTagModifier = camelToPathPiece' 2
, fieldLabelModifier = camelToPathPiece' 2
, sumEncoding = UntaggedValue
, unwrapUnaryRecords = True
} ''LogDestination
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''LogSettings } ''LogSettings
deriveFromJSON defaultOptions ''Ldap.Scope deriveFromJSON defaultOptions ''Ldap.Scope
deriveFromJSON defaultOptions deriveFromJSON defaultOptions
{ fieldLabelModifier = intercalate "-" . map toLower . drop 2 . splitCamel { fieldLabelModifier = camelToPathPiece' 2
} ''UserDefaultConf } ''UserDefaultConf
instance FromJSON LdapConf where instance FromJSON LdapConf where

View File

@ -5,8 +5,9 @@
--color-success: #23d160; --color-success: #23d160;
--color-info: #c4c4c4; --color-info: #c4c4c4;
--color-lightblack: #1A2A36; --color-lightblack: #1A2A36;
--color-lightwhite: #FCFFFA; --color-lightwhite: #fcfffa;
--color-grey: #B1B5C0; --color-grey: #B1B5C0;
--color-grey-light: #f4f5f6;
--color-font: #34303a; --color-font: #34303a;
--color-fontsec: #5b5861; --color-fontsec: #5b5861;
@ -515,7 +516,7 @@ section {
padding: 0 0 12px; padding: 0 0 12px;
margin: 0 0 12px; margin: 0 0 12px;
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid #d3d3d3;
} }
section:last-of-type { section:last-of-type {

View File

@ -1,17 +1,16 @@
$newline never $newline never
$if hasPageActions $if hasPageActions
<div .page-nav-prime> <div .page-nav-prime>
<ul .pagenav__list> $forall (MenuItem{menuItemLabel, menuItemType, menuItemModal}, menuIdent, route) <- menuTypes
$forall (MenuItem{menuItemLabel, menuItemType, menuItemModal}, menuIdent, route) <- menuTypes $case menuItemType
$case menuItemType $of PageActionPrime
$of PageActionPrime <div .pagenav__list-item>
<li .pagenav__list-item> $if menuItemModal
$if menuItemModal <div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic> <a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel}
<a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel} $of PageActionSecondary
$of PageActionSecondary <div .pagenav__list-item>
<li .pagenav__list-item> $if menuItemModal
$if menuItemModal <div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic> <a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel}
<a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel} $of _
$of _

View File

@ -1,19 +1,10 @@
.page-nav-prime { .page-nav-prime {
margin: 4px 0 13px; margin: 10px 0 20px;
border-left: 2px solid #c3c3c3; background-color: var(--color-grey-light);
padding-left: 10px;
}
.pagenav__list {
display: block;
margin-left: 0;
} }
.pagenav__list-item { .pagenav__list-item {
display: inline-block; display: inline-block;
padding: 15px;
&:not(:last-child) { box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
margin-right: 7px;
padding-right: 7px;
}
} }

View File

@ -21,7 +21,7 @@ sampleCron :: Natural -> Cron -> [UTCTime]
sampleCron n = go n baseTime Nothing sampleCron n = go n baseTime Nothing
where where
go 0 _ _ _ = [] go 0 _ _ _ = []
go (pred -> n') t mPrev cron = case nextCronMatch utcTZ mPrev t cron of go (pred -> n') t mPrev cron = case nextCronMatch utcTZ mPrev 0 t cron of
MatchAsap -> t : go n' t (Just t) cron MatchAsap -> t : go n' t (Just t) cron
MatchAt t' -> t' : go n' t' (Just t') cron MatchAt t' -> t' : go n' t' (Just t') cron
MatchNone -> [] MatchNone -> []

View File

@ -12,6 +12,7 @@ import Data.Pool (destroyAllResources)
import Database.Persist.Postgresql import Database.Persist.Postgresql
import Control.Monad.Logger import Control.Monad.Logger
import Control.Monad.Trans.Resource
import System.Console.GetOpt import System.Console.GetOpt
import System.Exit (exitWith, ExitCode(..)) import System.Exit (exitWith, ExitCode(..))
@ -50,6 +51,7 @@ main = do
DBTruncate -> db $ do DBTruncate -> db $ do
foundation <- getYesod foundation <- getYesod
stopJobCtl foundation stopJobCtl foundation
release . fst $ appLogger foundation
liftIO . destroyAllResources $ appConnPool foundation liftIO . destroyAllResources $ appConnPool foundation
truncateDb truncateDb
DBMigrate -> db $ return () DBMigrate -> db $ return ()

View File

@ -7,7 +7,6 @@ import TestImport
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Yesod.Core.Handler (toTextUrl) import Yesod.Core.Handler (toTextUrl)
import Yesod.Core.Unsafe (fakeHandlerGetLogger)
spec :: Spec spec :: Spec
spec = withApp $ do spec = withApp $ do
@ -15,8 +14,7 @@ spec = withApp $ do
it "asserts no access to my-account for anonymous users" $ do it "asserts no access to my-account for anonymous users" $ do
get ProfileR get ProfileR
app <- getTestYesod loginText <- runHandler . toTextUrl $ AuthR LoginR
loginText <- fakeHandlerGetLogger appLogger app (toTextUrl $ AuthR LoginR)
assertHeader "Location" $ encodeUtf8 loginText assertHeader "Location" $ encodeUtf8 loginText

View File

@ -44,7 +44,8 @@ runDBWithApp app query = liftIO $ runSqlPersistMPool query (appConnPool app)
runHandler :: Handler a -> YesodExample UniWorX a runHandler :: Handler a -> YesodExample UniWorX a
runHandler handler = do runHandler handler = do
app <- getTestYesod app <- getTestYesod
fakeHandlerGetLogger appLogger app handler logger <- liftIO . readTVarIO . snd $ appLogger app
fakeHandlerGetLogger (const logger) app handler
withApp :: YSpec UniWorX -> Spec withApp :: YSpec UniWorX -> Spec