Merge branch 'master' into babel

This commit is contained in:
Felix Hamann 2019-05-28 20:59:28 +02:00
commit 86ea9d4256
57 changed files with 1027 additions and 832 deletions

View File

@ -30,9 +30,14 @@ session-timeout: 7200
jwt-expiration: 604800 jwt-expiration: 604800
jwt-encoding: HS256 jwt-encoding: HS256
maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728" maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728"
health-check-interval: "_env:HEALTHCHECK_INTERVAL:600" # or WATCHDOG_USEC/2, whichever is smaller health-check-interval:
health-check-http: "_env:HEALTHCHECK_HTTP:true" matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600"
http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE:600"
ldap-admins: "_env:HEALTHCHECK_INTERVAL_LDAP_ADMINS:600"
smtp-connect: "_env:HEALTHCHECK_INTERVAL_SMTP_CONNECT:600"
widget-memcached: "_env:HEALTHCHECK_INTERVAL_WIDGET_MEMCACHED:600"
health-check-delay-notify: "_env:HEALTHCHECK_DELAY_NOTIFY:true" health-check-delay-notify: "_env:HEALTHCHECK_DELAY_NOTIFY:true"
health-check-http: "_env:HEALTHCHECK_HTTP:true" # Can we assume, that we can reach ourselves under APPROOT via HTTP (reverse proxies or firewalls might prevent this)?
log-settings: log-settings:
detailed: "_env:DETAILED_LOGGING:false" detailed: "_env:DETAILED_LOGGING:false"

View File

@ -254,6 +254,7 @@ export const interactiveFieldset = {
* *
* Attribute: [none] * Attribute: [none]
* (automatically setup on all form tags that dont automatically submit, see AutoSubmitButtonUtil) * (automatically setup on all form tags that dont automatically submit, see AutoSubmitButtonUtil)
* Does not setup on forms that have uw-no-navigate-away-prompt
* *
* Example usage: * Example usage:
* (any page with a form) * (any page with a form)
@ -261,6 +262,7 @@ export const interactiveFieldset = {
var NAVIGATE_AWAY_PROMPT_UTIL_NAME = 'navigateAwayPrompt'; var NAVIGATE_AWAY_PROMPT_UTIL_NAME = 'navigateAwayPrompt';
var NAVIGATE_AWAY_PROMPT_UTIL_SELECTOR = 'form'; var NAVIGATE_AWAY_PROMPT_UTIL_SELECTOR = 'form';
var NAVIGATE_AWAY_PROMPT_UTIL_OPTOUT = '[uw-no-navigate-away-prompt]';
var NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS = 'navigate-away-prompt--initialized'; var NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS = 'navigate-away-prompt--initialized';
@ -282,6 +284,10 @@ var navigateAwayPromptUtil = function(element) {
return false; return false;
} }
if (element.matches(NAVIGATE_AWAY_PROMPT_UTIL_OPTOUT)) {
return false;
}
window.addEventListener('beforeunload', beforeUnloadHandler); window.addEventListener('beforeunload', beforeUnloadHandler);
element.addEventListener('submit', function() { element.addEventListener('submit', function() {

View File

@ -126,6 +126,7 @@ dependencies:
- streaming-commons - streaming-commons
- hourglass - hourglass
- unix - unix
- stm-delay
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving

View File

@ -19,7 +19,7 @@ module Application
import Control.Monad.Logger (liftLoc, LoggingT(..), MonadLoggerIO(..)) import Control.Monad.Logger (liftLoc, LoggingT(..), MonadLoggerIO(..))
import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr, import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr,
pgPoolSize, runSqlPool) pgPoolSize, runSqlPool)
import Import import Import hiding (cancel)
import Language.Haskell.TH.Syntax (qLocation) import Language.Haskell.TH.Syntax (qLocation)
import Network.Wai (Middleware) import Network.Wai (Middleware)
import Network.Wai.Handler.Warp (Settings, defaultSettings, import Network.Wai.Handler.Warp (Settings, defaultSettings,
@ -75,17 +75,22 @@ import System.Exit
import qualified Database.Memcached.Binary.IO as Memcached import qualified Database.Memcached.Binary.IO as Memcached
import qualified System.Systemd.Daemon as Systemd import qualified System.Systemd.Daemon as Systemd
import Control.Concurrent.Async.Lifted.Safe (async, waitAnyCancel) import Control.Concurrent.Async.Lifted.Safe
import System.Environment (lookupEnv) import System.Environment (lookupEnv)
import System.Posix.Process (getProcessID) import System.Posix.Process (getProcessID)
import System.Posix.Signals (SignalInfo(..), installHandler, sigTERM) import System.Posix.Signals (SignalInfo(..), installHandler, sigTERM)
import qualified System.Posix.Signals as Signals (Handler(..)) import qualified System.Posix.Signals as Signals (Handler(..))
import Control.Monad.Trans.State (execStateT)
import Network (socketPort) import Network (socketPort)
import qualified Network.Socket as Socket (close) import qualified Network.Socket as Socket (close)
import Control.Concurrent.STM.Delay
import Control.Monad.STM (retry)
import qualified Data.Set as Set
import Data.Semigroup (Max(..), Min(..))
-- Import all relevant handler modules here. -- Import all relevant handler modules here.
-- (HPack takes care to add new modules to our cabal file nowadays.) -- (HPack takes care to add new modules to our cabal file nowadays.)
import Handler.Common import Handler.Common
@ -152,7 +157,7 @@ makeFoundation appSettings'@AppSettings{..} = do
appJobCtl <- liftIO $ newTVarIO Map.empty appJobCtl <- liftIO $ newTVarIO Map.empty
appCronThread <- liftIO newEmptyTMVarIO appCronThread <- liftIO newEmptyTMVarIO
appHealthReport <- liftIO $ newTVarIO Nothing appHealthReport <- liftIO $ newTVarIO Set.empty
-- 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
@ -333,7 +338,12 @@ warpSettings foundation = defaultSettings
if if
| foundation ^. _appHealthCheckDelayNotify | foundation ^. _appHealthCheckDelayNotify
-> void . fork $ do -> void . fork $ do
atomically $ readTVar (foundation ^. _appHealthReport) >>= guard . maybe False ((== HealthSuccess) . classifyHealthReport . snd) let activeChecks = Set.fromList universeF
& Set.filter (is _Just . (foundation ^. _appHealthCheckInterval))
atomically $ do
results <- readTVar $ foundation ^. _appHealthReport
guard $ activeChecks == Set.map (classifyHealthReport . snd) results
guard . (== Min HealthSuccess) $ foldMap (Min . healthReportStatus . snd) results
notifyReady notifyReady
| otherwise | otherwise
-> notifyReady -> notifyReady
@ -354,19 +364,8 @@ warpSettings foundation = defaultSettings
getAppDevSettings, getAppSettings :: MonadIO m => m AppSettings getAppDevSettings, getAppSettings :: MonadIO m => m AppSettings
getAppDevSettings = liftIO $ adjustSettings =<< loadYamlSettings [configSettingsYml] [configSettingsYmlValue] useEnv getAppDevSettings = liftIO $ loadYamlSettings [configSettingsYml] [configSettingsYmlValue] useEnv
getAppSettings = liftIO $ adjustSettings =<< loadYamlSettingsArgs [configSettingsYmlValue] useEnv getAppSettings = liftIO $ loadYamlSettingsArgs [configSettingsYmlValue] useEnv
adjustSettings :: MonadIO m => AppSettings -> m AppSettings
adjustSettings = execStateT $ do
watchdogMicroSec <- liftIO $ (>>= readMay) <$> lookupEnv "WATCHDOG_USEC"
watchdogProcess <- liftIO $ (>>= fmap fromInteger . readMay) <$> lookupEnv "WATCHDOG_PID"
myProcessID <- liftIO getProcessID
case watchdogMicroSec of
Just wInterval
| maybe True (== myProcessID) watchdogProcess
-> _appHealthCheckInterval %= min (fromRational $ (toRational wInterval / 1e6) / 2)
_other -> return ()
-- | main function for use by yesod devel -- | main function for use by yesod devel
develMain :: IO () develMain :: IO ()
@ -417,7 +416,47 @@ appMain = runResourceT $ do
case didStore of case didStore of
Just () -> $logInfoS "shutdown" "Stored all bound sockets for restart" Just () -> $logInfoS "shutdown" "Stored all bound sockets for restart"
Nothing -> forM_ sockets $ liftIO . Socket.close Nothing -> forM_ sockets $ liftIO . Socket.close
liftIO . throwTo mainThreadId . ExitFailure $ 0b10000000 + fromIntegral siginfoSignal liftIO $ throwTo mainThreadId ExitSuccess
watchdogMicroSec <- liftIO $ (>>= readMay) <$> lookupEnv "WATCHDOG_USEC"
watchdogProcess <- liftIO $ (>>= fmap fromInteger . readMay) <$> lookupEnv "WATCHDOG_PID"
myProcessID <- liftIO getProcessID
case watchdogMicroSec of
Just wInterval
| maybe True (== myProcessID) watchdogProcess
-> let notifyWatchdog :: IO ()
notifyWatchdog = runAppLoggingT foundation $ go Nothing
where
go pStatus = do
d <- liftIO . newDelay . floor $ wInterval % 2
status <- atomically $ asum
[ Nothing <$ waitDelay d
, Just <$> do
results <- readTVar $ foundation ^. _appHealthReport
case fromNullable results of
Nothing -> retry
Just rs -> do
let status = ofoldMap1 (Max *** Min . healthReportStatus) rs
guard $ pStatus /= Just status
return status
]
case status of
Just (_, Min status') -> do
$logInfoS "NotifyStatus" $ toPathPiece status'
liftIO . void . Systemd.notifyStatus . unpack $ toPathPiece status'
Nothing -> return ()
case status of
Just (_, Min HealthSuccess) -> do
$logInfoS "NotifyWatchdog" "Notify"
liftIO $ void Systemd.notifyWatchdog
_other -> return ()
go status
in void $ allocate (async notifyWatchdog >>= \a -> a <$ link a) cancel
_other -> return ()
let runWarp socket = runSettingsSocket (warpSettings foundation) socket app let runWarp socket = runSettingsSocket (warpSettings foundation) socket app
case sockets of case sockets of

View File

@ -57,7 +57,7 @@ dummyLogin = AuthPlugin{..}
{ formMethod = POST { formMethod = POST
, formAction = Just . SomeRoute . toMaster $ PluginR "dummy" [] , formAction = Just . SomeRoute . toMaster $ PluginR "dummy" []
, formEncoding = loginEnctype , formEncoding = loginEnctype
, formAttrs = [] , formAttrs = [("uw-no-navigate-away-prompt","")]
, formSubmit = FormSubmit , formSubmit = FormSubmit
, formAnchor = Just "login--dummy" :: Maybe Text , formAnchor = Just "login--dummy" :: Maybe Text
} }

View File

@ -117,7 +117,7 @@ campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
{ formMethod = POST { formMethod = POST
, formAction = Just . SomeRoute . toMaster $ PluginR "LDAP" [] , formAction = Just . SomeRoute . toMaster $ PluginR "LDAP" []
, formEncoding = loginEnctype , formEncoding = loginEnctype
, formAttrs = [] , formAttrs = [("uw-no-navigate-away-prompt","")]
, formSubmit = FormSubmit , formSubmit = FormSubmit
, formAnchor = Just "login--campus" :: Maybe Text , formAnchor = Just "login--campus" :: Maybe Text
} }

View File

@ -93,7 +93,7 @@ hashLogin pwHashAlgo = AuthPlugin{..}
{ formMethod = POST { formMethod = POST
, formAction = Just . SomeRoute . toMaster $ PluginR "PWHash" [] , formAction = Just . SomeRoute . toMaster $ PluginR "PWHash" []
, formEncoding = loginEnctype , formEncoding = loginEnctype
, formAttrs = [] , formAttrs = [("uw-no-navigate-away-prompt","")]
, formSubmit = FormSubmit , formSubmit = FormSubmit
, formAnchor = Just "login--hash" :: Maybe Text , formAnchor = Just "login--hash" :: Maybe Text
} }

View File

@ -1,3 +1,4 @@
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module CryptoID module CryptoID

View File

@ -11,6 +11,11 @@ import Data.Binary.SerializationLength
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import System.FilePath (FilePath) import System.FilePath (FilePath)
import Data.Binary (Binary)
import qualified Data.Binary as Binary
import Database.Persist.Sql
decCryptoIDs :: [Name] -> DecsQ decCryptoIDs :: [Name] -> DecsQ
decCryptoIDs = fmap concat . mapM decCryptoID decCryptoIDs = fmap concat . mapM decCryptoID
@ -21,6 +26,11 @@ decCryptoIDs = fmap concat . mapM decCryptoID
instance HasFixedSerializationLength $(t) where instance HasFixedSerializationLength $(t) where
type SerializationLength $(t) = SerializationLength Int64 type SerializationLength $(t) = SerializationLength Int64
instance {-# OVERLAPPING #-} Binary $(t) where
put = Binary.put . fromSqlKey
putList = Binary.putList . map fromSqlKey
get = toSqlKey <$> Binary.get
type instance CryptoIDNamespace a $(t) = $(litT $ strTyLit ns) type instance CryptoIDNamespace a $(t) = $(litT $ strTyLit ns)
|] |]

View File

@ -14,9 +14,13 @@ import Data.Binary (Binary)
import Data.HashMap.Strict.Instances () import Data.HashMap.Strict.Instances ()
import Data.Vector.Instances () import Data.Vector.Instances ()
import Model.Types.TH.JSON (derivePersistFieldJSON)
instance MonadThrow Parser where instance MonadThrow Parser where
throwM = fail . show throwM = fail . show
instance Binary Value instance Binary Value
derivePersistFieldJSON ''Value

View File

@ -0,0 +1,18 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.Time.Calendar.Instances
(
) where
import ClassyPrelude
import Data.Binary (Binary)
import qualified Data.Binary as Binary
deriving newtype instance Hashable Day
instance Binary Day where
get = ModifiedJulianDay <$> Binary.get
put = Binary.put . toModifiedJulianDay

View File

@ -11,14 +11,17 @@ import Data.Time.Clock
import Data.Binary (Binary) import Data.Binary (Binary)
import qualified Data.Binary as Binary import qualified Data.Binary as Binary
import Data.Time.Calendar.Instances ()
instance Hashable DiffTime where
hashWithSalt s = hashWithSalt s . toRational
deriving instance Generic UTCTime deriving instance Generic UTCTime
instance Hashable UTCTime
instance Binary Day where
get = ModifiedJulianDay <$> Binary.get
put = Binary.put . toModifiedJulianDay
instance Binary DiffTime where instance Binary DiffTime where
get = fromRational <$> Binary.get get = fromRational <$> Binary.get
put = Binary.put . toRational put = Binary.put . toRational

View File

@ -0,0 +1,14 @@
{-# OPTIONS -fno-warn-orphans #-}
module Data.Time.Format.Instances
(
) where
import qualified Language.Haskell.TH.Syntax as TH
import Data.Time.Format
import Data.Time.LocalTime.Instances ()
deriving instance TH.Lift TimeLocale

View File

@ -0,0 +1,23 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.Time.LocalTime.Instances
(
) where
import ClassyPrelude
import Data.Time.LocalTime
import Data.Binary (Binary)
import qualified Language.Haskell.TH.Syntax as TH
deriving instance Generic TimeOfDay
deriving instance Typeable TimeOfDay
instance Hashable TimeOfDay
instance Binary TimeOfDay
deriving instance TH.Lift TimeZone

View File

@ -0,0 +1,27 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.UUID.Instances
() where
import ClassyPrelude
import Data.UUID (UUID)
import qualified Data.UUID as UUID
import Database.Persist.Sql
import Web.PathPieces
instance PathPiece UUID where
fromPathPiece = UUID.fromString . unpack
toPathPiece = pack . UUID.toString
instance PersistField UUID where
toPersistValue = PersistDbSpecific . UUID.toASCIIBytes
fromPersistValue (PersistText t) = maybe (Left "Failed to parse UUID") Right $ UUID.fromText t
fromPersistValue (PersistByteString bs) = maybe (Left "Failed to parse UUID") Right $ UUID.fromASCIIBytes bs
fromPersistValue (PersistDbSpecific bs) = maybe (Left "Failed to parse UUID") Right $ UUID.fromASCIIBytes bs
fromPersistValue x = Left $ "Expected UUID, received: " <> tshow x
instance PersistFieldSql UUID where
sqlType _ = SqlOther "uuid"

View File

@ -0,0 +1,17 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.Universe.Instances.Reverse.MonoTraversable
(
) where
import Data.Universe
import Data.MonoTraversable
import Data.Universe.Instances.Reverse
type instance Element (a -> b) = b
instance Finite a => MonoFoldable (a -> b)
instance (Ord a, Finite a) => MonoTraversable (a -> b)

View File

@ -0,0 +1,23 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE UndecidableInstances #-}
module Database.Persist.Class.Instances
(
) where
import ClassyPrelude
import Database.Persist.Class
import Database.Persist.Types.Instances ()
import Data.Binary (Binary)
import qualified Data.Binary as Binary
instance PersistEntity record => Hashable (Key record) where
hashWithSalt s = hashWithSalt s . toPersistValue
instance PersistEntity record => Binary (Key record) where
put = Binary.put . toPersistValue
putList = Binary.putList . map toPersistValue
get = either (fail . unpack) return . fromPersistValue =<< Binary.get

View File

@ -1,33 +0,0 @@
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Database.Persist.Sql.Instances
(
) where
import ClassyPrelude.Yesod
import Data.Binary (Binary)
import qualified Data.Binary as B
import Database.Persist.Sql
instance Binary (BackendKey SqlWriteBackend) where
put = B.put . unSqlWriteBackendKey
putList = B.putList . map unSqlWriteBackendKey
get = SqlWriteBackendKey <$> B.get
instance Binary (BackendKey SqlReadBackend) where
put = B.put . unSqlReadBackendKey
putList = B.putList . map unSqlReadBackendKey
get = SqlReadBackendKey <$> B.get
instance Binary (BackendKey SqlBackend) where
put = B.put . unSqlBackendKey
putList = B.putList . map unSqlBackendKey
get = SqlBackendKey <$> B.get
instance {-# OVERLAPPABLE #-} ToBackendKey SqlBackend record => Binary (Key record) where
put = B.put . fromSqlKey
putList = B.putList . map fromSqlKey
get = toSqlKey <$> B.get

View File

@ -1,4 +1,3 @@
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Database.Persist.Types.Instances module Database.Persist.Types.Instances
@ -6,7 +5,18 @@ module Database.Persist.Types.Instances
) where ) where
import ClassyPrelude import ClassyPrelude
import Database.Persist.Types import Database.Persist.Types
instance (Hashable record, Hashable (Key record)) => Hashable (Entity record) where import Data.Time.Calendar.Instances ()
s `hashWithSalt` Entity{..} = s `hashWithSalt` entityKey `hashWithSalt` entityVal import Data.Time.LocalTime.Instances ()
import Data.Time.Clock.Instances ()
import Data.Binary (Binary)
deriving instance Generic PersistValue
deriving instance Typeable PersistValue
instance Hashable PersistValue
instance Binary PersistValue

View File

@ -130,7 +130,7 @@ data UniWorX = UniWorX
, appSessionKey :: ClientSession.Key , appSessionKey :: ClientSession.Key
, appSecretBoxKey :: SecretBox.Key , appSecretBoxKey :: SecretBox.Key
, appJSONWebKeySet :: Jose.JwkSet , appJSONWebKeySet :: Jose.JwkSet
, appHealthReport :: TVar (Maybe (UTCTime, HealthReport)) , appHealthReport :: TVar (Set (UTCTime, HealthReport))
} }
makeLenses_ ''UniWorX makeLenses_ ''UniWorX

View File

@ -9,55 +9,71 @@ import Utils.Lens
import qualified Data.UUID as UUID import qualified Data.UUID as UUID
import Data.Semigroup (Min(..), Max(..))
import qualified Data.Set as Set
import Control.Concurrent.STM.Delay
getHealthR :: Handler TypedContent getHealthR :: Handler TypedContent
getHealthR = do getHealthR = do
healthReport' <- liftIO . readTVarIO =<< getsYesod appHealthReport reportStore <- getsYesod appHealthReport
let healthReports' <- liftIO $ readTVarIO reportStore
handleMissing = do
interval <- getsYesod $ round . (* 1e6) . toRational . view _appHealthCheckInterval
reportStore <- getsYesod appHealthReport
waitResult <- threadDelay interval `race` atomically (readTVar reportStore >>= guard . is _Just)
case waitResult of
Left () -> fail "System is not generating HealthReports"
Right _ -> redirect HealthR
(lastUpdated, healthReport) <- maybe handleMissing return healthReport'
interval <- getsYesod $ view _appHealthCheckInterval interval <- getsYesod $ view _appHealthCheckInterval
instanceId <- getsYesod appInstanceID
setWeakEtagHashable (instanceId, lastUpdated) case fromNullable healthReports' of
expiresAt $ interval `addUTCTime` lastUpdated Nothing -> do
setLastModified lastUpdated let Min (NTop minInterval) = ofoldMap1 (Min . NTop) $ impureNonNull interval
delay <- for minInterval $ \minInterval' -> liftIO . newDelay . round $ toRational minInterval' * 1e6
let status waitResult <- atomically $ maybe (pure $ Left False) (fmap (const $ Left True) . waitDelay) delay <|> (fmap Right . assertM (not. Set.null) $ readTVar reportStore)
| HealthSuccess <- classifyHealthReport healthReport case waitResult of
= ok200 Left False -> sendResponseStatus noContent204 ()
| otherwise Left True -> fail "System is not generating HealthReports"
= internalServerError500 Right _ -> redirect HealthR
sendResponseStatus status <=< selectRep $ do Just healthReports -> do
provideRep . siteLayoutMsg MsgHealthReport $ do let (Max lastUpdated, Min status) = ofoldMap1 (Max *** Min . healthReportStatus) healthReports
setTitleI MsgHealthReport reportNextUpdate (lastCheck, classifyHealthReport -> kind)
let HealthReport{..} = healthReport = fromMaybe 0 (interval kind) `addUTCTime` lastCheck
[whamlet| Max nextUpdate = ofoldMap1 (Max . reportNextUpdate) healthReports
$newline never instanceId <- getsYesod appInstanceID
<dl .deflist>
<dt .deflist__dt>_{MsgHealthMatchingClusterConfig} setWeakEtagHashable (instanceId, lastUpdated)
<dd .deflist__dd>#{boolSymbol healthMatchingClusterConfig} expiresAt nextUpdate
$maybe httpReachable <- healthHTTPReachable setLastModified lastUpdated
<dt .deflist__dt>_{MsgHealthHTTPReachable}
<dd .deflist__dd>#{boolSymbol httpReachable} let status'
$maybe ldapAdmins <- healthLDAPAdmins | HealthSuccess <- status
<dt .deflist__dt>_{MsgHealthLDAPAdmins} = ok200
<dd .deflist__dd>#{textPercent ldapAdmins} | otherwise
$maybe smtpConnect <- healthSMTPConnect = internalServerError500
<dt .deflist__dt>_{MsgHealthSMTPConnect} sendResponseStatus status' <=< selectRep $ do
<dd .deflist__dd>#{boolSymbol smtpConnect} provideRep . siteLayoutMsg MsgHealthReport $ do
$maybe widgetMemcached <- healthWidgetMemcached setTitleI MsgHealthReport
<dt .deflist__dt>_{MsgHealthWidgetMemcached} [whamlet|
<dd .deflist__dd>#{boolSymbol widgetMemcached} $newline never
|] <dl .deflist>
provideJson healthReport $forall (_, report) <- healthReports'
provideRep . return . Builder.toLazyText $ Aeson.encodePrettyToTextBuilder healthReport $case report
$of HealthMatchingClusterConfig passed
<dt .deflist__dt>_{MsgHealthMatchingClusterConfig}
<dd .deflist__dd>#{boolSymbol passed}
$of HealthHTTPReachable (Just passed)
<dt .deflist__dt>_{MsgHealthHTTPReachable}
<dd .deflist__dd>#{boolSymbol passed}
$of HealthLDAPAdmins (Just found)
<dt .deflist__dt>_{MsgHealthLDAPAdmins}
<dd .deflist__dd>#{textPercent found}
$of HealthSMTPConnect (Just passed)
<dt .deflist__dt>_{MsgHealthSMTPConnect}
<dd .deflist__dd>#{boolSymbol passed}
$of HealthWidgetMemcached (Just passed)
<dt .deflist__dt>_{MsgHealthWidgetMemcached}
<dd .deflist__dd>#{boolSymbol passed}
$of _
|]
provideJson healthReports
provideRep . return . Builder.toLazyText $ Aeson.encodePrettyToTextBuilder healthReports
getInstanceR :: Handler TypedContent getInstanceR :: Handler TypedContent
getInstanceR = do getInstanceR = do

View File

@ -20,9 +20,6 @@ import Data.Map ((!), (!?))
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set import qualified Data.Set as Set
import Data.Aeson.TH
import Data.Aeson.Types (ToJSONKey(..), FromJSONKey(..), toJSONKeyText, FromJSONKeyFunction(..))
data RecipientGroup = RGCourseParticipants | RGCourseLecturers | RGCourseCorrectors | RGCourseTutors data RecipientGroup = RGCourseParticipants | RGCourseLecturers | RGCourseCorrectors | RGCourseTutors
| RGTutorialParticipants | RGTutorialParticipants

View File

@ -1,4 +1,4 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- tupleBoxCoord
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Handler.Utils.Form.MassInput module Handler.Utils.Form.MassInput
@ -20,8 +20,6 @@ import Utils.Lens
import Handler.Utils.Form.MassInput.Liveliness import Handler.Utils.Form.MassInput.Liveliness
import Handler.Utils.Form.MassInput.TH import Handler.Utils.Form.MassInput.TH
import Data.Aeson hiding (Result(..))
import Algebra.Lattice hiding (join) import Algebra.Lattice hiding (join)
import Text.Blaze (Markup) import Text.Blaze (Markup)

View File

@ -32,7 +32,6 @@ import qualified Data.HashSet as HashSet
import Data.Aeson (fromJSON) import Data.Aeson (fromJSON)
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import Data.Aeson.TH
import Data.Proxy (Proxy(..)) import Data.Proxy (Proxy(..))
import Data.Typeable import Data.Typeable

View File

@ -1,5 +1,3 @@
{-# OPTIONS -fno-warn-orphans #-}
module Handler.Utils.Table.Pagination module Handler.Utils.Table.Pagination
( module Handler.Utils.Table.Pagination.Types ( module Handler.Utils.Table.Pagination.Types
, SortColumn(..), SortDirection(..) , SortColumn(..), SortDirection(..)

View File

@ -1,107 +1,17 @@
module Import.NoFoundation module Import.NoFoundation
( module Import ( module Import
, MForm
) where ) where
import ClassyPrelude.Yesod as Import hiding (formatTime, derivePersistFieldJSON, addMessage, addMessageI, (.=), MForm, Proxy, foldlM, static, boolField, identifyForm, cons) import Import.NoModel as Import
import Model as Import import Model as Import
import Model.Types.JSON as Import
import Model.Migration as Import import Model.Migration as Import
import Model.Rating as Import import Model.Rating as Import
import Model.Submission as Import import Model.Submission as Import
import Model.Tokens as Import import Model.Tokens as Import
import Utils.Tokens as Import
import Utils.Frontend.Modal as Import
import Settings as Import import Settings as Import
import Settings.StaticFiles as Import import Settings.StaticFiles as Import
import Yesod.Auth as Import
import Yesod.Core.Types as Import (loggerSet)
import Yesod.Default.Config2 as Import
import Utils as Import
import Utils.Frontend.Modal as Import
import Utils.Frontend.I18n as Import
import Utils.DB as Import
import Yesod.Core.Json as Import (provideJson)
import Yesod.Core.Types.Instances as Import (CachedMemoT(..))
import Language.Haskell.TH.Instances as Import ()
import Utils.Tokens as Import
import Data.Fixed as Import
import CryptoID as Import import CryptoID as Import
import Data.UUID as Import (UUID)
import Text.Lucius as Import
import Text.Shakespeare.Text as Import hiding (text, stext)
import Data.Universe as Import
import Data.Universe.TH as Import
import Data.Pool as Import (Pool)
import Network.HaskellNet.SMTP as Import (SMTPConnection)
import Mail as Import
import Data.Data as Import (Data)
import Data.Typeable as Import (Typeable)
import GHC.Generics as Import (Generic)
import GHC.Exts as Import (IsList)
import Data.Hashable as Import
import Data.List.NonEmpty as Import (NonEmpty(..), nonEmpty)
import Data.List.NonEmpty.Instances as Import ()
import Data.NonNull.Instances as Import ()
import Data.Text.Encoding.Error as Import(UnicodeException(..))
import Data.Semigroup as Import (Semigroup)
import Data.Monoid as Import (Last(..), First(..), Any(..), All(..), Sum(..))
import Data.Monoid.Instances as Import ()
import Data.Set.Instances as Import ()
import Data.HashMap.Strict.Instances as Import ()
import Data.HashSet.Instances as Import ()
import Data.Vector.Instances as Import ()
import Data.Time.Clock.Instances as Import ()
import Data.Binary as Import (Binary)
import Control.Monad.Morph as Import (MFunctor(..))
import Control.Monad.Trans.Resource as Import (ReleaseKey)
import Network.Mail.Mime.Instances as Import ()
import Yesod.Core.Instances as Import ()
import Data.Aeson.Types.Instances as Import ()
import Ldap.Client.Pool as Import
import Database.Esqueleto.Instances as Import ()
import Database.Persist.Sql.Instances as Import ()
import Database.Persist.Sql as Import (SqlReadT,SqlWriteT)
import Database.Persist.Types.Instances as Import ()
import Numeric.Natural.Instances as Import ()
import System.Random as Import (Random)
import Control.Monad.Random.Class as Import (MonadRandom(..))
import Text.Blaze.Instances as Import ()
import Jose.Jwt.Instances as Import ()
import Jose.Jwt as Import (Jwt)
import Web.PathPieces.Instances as Import ()
import Data.Time.Calendar as Import
import Data.Time.Clock as Import
import Data.Time.LocalTime as Import hiding (utcToLocalTime, localTimeToUTC)
import Time.Types as Import (WeekDay(..))
import Time.Types.Instances as Import ()
import Data.CaseInsensitive as Import (CI, FoldCase(..), foldedCase)
import Data.Ratio as Import ((%))
import Network.Mime as Import
import Control.Monad.Trans.RWS (RWST)
type MForm m = RWST (Maybe (Env, FileEnv), HandlerSite m, [Lang]) Enctype Ints m

105
src/Import/NoModel.hs Normal file
View File

@ -0,0 +1,105 @@
module Import.NoModel
( module Import
, MForm
) where
import ClassyPrelude.Yesod as Import hiding (formatTime, derivePersistFieldJSON, addMessage, addMessageI, (.=), MForm, Proxy, foldlM, static, boolField, identifyForm, cons)
import Model.Types.TH.JSON as Import
import Model.Types.TH.Wordlist as Import
import Mail as Import
import Yesod.Auth as Import
import Yesod.Core.Types as Import (loggerSet)
import Yesod.Default.Config2 as Import
import Yesod.Core.Json as Import (provideJson)
import Yesod.Core.Types.Instances as Import (CachedMemoT(..))
import Utils as Import
import Utils.Frontend.I18n as Import
import Utils.DB as Import
import Data.Fixed as Import
import Data.UUID as Import (UUID)
import Data.CaseInsensitive as Import (CI, FoldCase(..), foldedCase)
import Text.Lucius as Import
import Text.Shakespeare.Text as Import hiding (text, stext)
import Data.Universe as Import
import Data.Universe.TH as Import
import Data.Pool as Import (Pool)
import Network.HaskellNet.SMTP as Import (SMTPConnection)
import Data.Data as Import (Data)
import Data.Typeable as Import (Typeable)
import GHC.Generics as Import (Generic)
import GHC.Exts as Import (IsList)
import Data.Ix as Import (Ix)
import Data.Hashable as Import
import Data.List.NonEmpty as Import (NonEmpty(..), nonEmpty)
import Data.Text.Encoding.Error as Import(UnicodeException(..))
import Data.Semigroup as Import (Semigroup)
import Data.Monoid as Import (Last(..), First(..), Any(..), All(..), Sum(..))
import Data.Binary as Import (Binary)
import Numeric.Natural as Import (Natural)
import Data.Ratio as Import ((%))
import Database.Persist.Sql as Import (SqlReadT, SqlWriteT, fromSqlKey, toSqlKey)
import Ldap.Client.Pool as Import
import System.Random as Import (Random(..))
import Control.Monad.Random.Class as Import (MonadRandom(..))
import Control.Monad.Morph as Import (MFunctor(..))
import Control.Monad.Trans.Resource as Import (ReleaseKey)
import Jose.Jwt as Import (Jwt)
import Data.Time.Calendar as Import
import Data.Time.Clock as Import
import Data.Time.LocalTime as Import hiding (utcToLocalTime, localTimeToUTC)
import Time.Types as Import (WeekDay(..))
import Network.Mime as Import
import Data.Aeson.TH as Import
import Data.Aeson.Types as Import (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), toJSONKeyText, FromJSONKeyFunction(..), ToJSONKeyFunction(..), Value)
import Language.Haskell.TH.Instances as Import ()
import Data.List.NonEmpty.Instances as Import ()
import Data.NonNull.Instances as Import ()
import Data.Monoid.Instances as Import ()
import Data.Set.Instances as Import ()
import Data.HashMap.Strict.Instances as Import ()
import Data.HashSet.Instances as Import ()
import Data.Vector.Instances as Import ()
import Data.Time.Clock.Instances as Import ()
import Data.Time.LocalTime.Instances as Import ()
import Data.Time.Calendar.Instances as Import ()
import Data.Time.Format.Instances as Import ()
import Time.Types.Instances as Import ()
import Network.Mail.Mime.Instances as Import ()
import Yesod.Core.Instances as Import ()
import Data.Aeson.Types.Instances as Import ()
import Database.Esqueleto.Instances as Import ()
import Numeric.Natural.Instances as Import ()
import Text.Blaze.Instances as Import ()
import Jose.Jwt.Instances as Import ()
import Web.PathPieces.Instances as Import ()
import Data.Universe.Instances.Reverse.MonoTraversable ()
import Database.Persist.Class.Instances as Import ()
import Database.Persist.Types.Instances as Import ()
import Data.UUID.Instances as Import ()
import System.FilePath.Instances as Import ()
import Control.Monad.Trans.RWS (RWST)
type MForm m = RWST (Maybe (Env, FileEnv), HandlerSite m, [Lang]) Enctype Ints m

View File

@ -32,6 +32,7 @@ import Control.Monad.Random (evalRand, mkStdGen, getRandomR)
import Cron import Cron
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import qualified Data.Set as Set
import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List.NonEmpty as NonEmpty
@ -51,8 +52,6 @@ import Data.Time.Zones
import Control.Concurrent.STM (retry) import Control.Concurrent.STM (retry)
import qualified System.Systemd.Daemon as Systemd
import Jobs.Handler.SendNotification import Jobs.Handler.SendNotification
import Jobs.Handler.SendTestEmail import Jobs.Handler.SendTestEmail
@ -284,21 +283,19 @@ handleJobs' wNum = C.mapM_ $ \jctl -> do
-- logDebugS logIdent $ tshow newCTab -- logDebugS logIdent $ tshow newCTab
mapReaderT (liftIO . atomically) $ mapReaderT (liftIO . atomically) $
lift . void . flip swapTMVar newCTab =<< asks jobCrontab lift . void . flip swapTMVar newCTab =<< asks jobCrontab
handleCmd JobCtlGenerateHealthReport = do handleCmd (JobCtlGenerateHealthReport kind) = do
hrStorage <- getsYesod appHealthReport hrStorage <- getsYesod appHealthReport
newReport@(classifyHealthReport -> newStatus) <- lift generateHealthReport newReport@(healthReportStatus -> newStatus) <- lift $ generateHealthReport kind
$logInfoS "HealthReport" $ toPathPiece newStatus $logInfoS (tshow kind) $ toPathPiece newStatus
unless (newStatus == HealthSuccess) $ do unless (newStatus == HealthSuccess) $ do
$logErrorS "HealthReport" $ tshow newReport $logErrorS (tshow kind) $ tshow newReport
liftIO $ do liftIO $ do
now <- getCurrentTime now <- getCurrentTime
atomically . writeTVar hrStorage $ Just (now, newReport) let updateReports = Set.insert (now, newReport)
. Set.filter (((/=) `on` classifyHealthReport) newReport . snd)
void . Systemd.notifyStatus . unpack $ toPathPiece newStatus atomically . modifyTVar' hrStorage $ force . updateReports
when (newStatus == HealthSuccess) $
void Systemd.notifyWatchdog
jLocked :: QueuedJobId -> (QueuedJob -> Handler a) -> Handler a jLocked :: QueuedJobId -> (QueuedJob -> Handler a) -> Handler a
jLocked jId act = do jLocked jId act = do

View File

@ -43,14 +43,17 @@ determineCrontab = execWriterT $ do
, cronNotAfter = Right CronNotScheduled , cronNotAfter = Right CronNotScheduled
} }
tell $ HashMap.singleton tell . flip foldMap universeF $ \kind ->
JobCtlGenerateHealthReport case appHealthCheckInterval kind of
Cron Just int -> HashMap.singleton
{ cronInitial = CronAsap (JobCtlGenerateHealthReport kind)
, cronRepeat = CronRepeatScheduled CronAsap Cron
, cronRateLimit = appHealthCheckInterval { cronInitial = CronAsap
, cronNotAfter = Right CronNotScheduled , cronRepeat = CronRepeatScheduled CronAsap
} , cronRateLimit = int
, cronNotAfter = Right CronNotScheduled
}
Nothing -> mempty
let let
sheetJobs (Entity nSheet Sheet{..}) = do sheetJobs (Entity nSheet Sheet{..}) = do

View File

@ -28,18 +28,13 @@ import qualified Network.HaskellNet.SMTP as SMTP
import Data.Pool (withResource) import Data.Pool (withResource)
generateHealthReport :: Handler HealthReport generateHealthReport :: HealthCheck -> Handler HealthReport
generateHealthReport generateHealthReport = $(dispatchTH ''HealthCheck)
= runConcurrently $ HealthReport
<$> Concurrently matchingClusterConfig
<*> Concurrently httpReachable
<*> Concurrently ldapAdmins
<*> Concurrently smtpConnect
<*> Concurrently widgetMemcached
matchingClusterConfig :: Handler Bool dispatchHealthCheckMatchingClusterConfig :: Handler HealthReport
-- ^ Can the cluster configuration be read from the database and does it match our configuration? -- ^ Can the cluster configuration be read from the database and does it match our configuration?
matchingClusterConfig = runDB $ and <$> forM universeF clusterSettingMatches dispatchHealthCheckMatchingClusterConfig
= fmap HealthMatchingClusterConfig . runDB $ and <$> forM universeF clusterSettingMatches
where where
clusterSettingMatches ClusterCryptoIDKey = do clusterSettingMatches ClusterCryptoIDKey = do
ourSetting <- getsYesod appCryptoIDKey ourSetting <- getsYesod appCryptoIDKey
@ -74,11 +69,11 @@ matchingClusterConfig = runDB $ and <$> forM universeF clusterSettingMatches
_other -> return Nothing _other -> return Nothing
httpReachable :: Handler (Maybe Bool) dispatchHealthCheckHTTPReachable :: Handler HealthReport
httpReachable = do dispatchHealthCheckHTTPReachable = HealthHTTPReachable <$> do
staticAppRoot <- getsYesod $ view _appRoot staticAppRoot <- getsYesod $ view _appRoot
doHTTP <- getsYesod $ view _appHealthCheckHTTP doHTTP <- getsYesod $ view _appHealthCheckHTTP
for (staticAppRoot <* guard doHTTP) $ \_textAppRoot -> do for (staticAppRoot <* guard doHTTP) $ \_ -> do
url <- getUrlRender <*> pure InstanceR url <- getUrlRender <*> pure InstanceR
baseRequest <- HTTP.parseRequest $ unpack url baseRequest <- HTTP.parseRequest $ unpack url
httpManager <- getsYesod appHttpManager httpManager <- getsYesod appHttpManager
@ -88,8 +83,8 @@ httpReachable = do
getsYesod $ (== clusterId) . appClusterID getsYesod $ (== clusterId) . appClusterID
ldapAdmins :: Handler (Maybe Rational) dispatchHealthCheckLDAPAdmins :: Handler HealthReport
ldapAdmins = do dispatchHealthCheckLDAPAdmins = HealthLDAPAdmins <$> do
ldapPool' <- getsYesod appLdapPool ldapPool' <- getsYesod appLdapPool
ldapConf' <- getsYesod $ view _appLdapConf ldapConf' <- getsYesod $ view _appLdapConf
ldapAdminUsers <- fmap (map E.unValue) . runDB . E.select . E.from $ \(user `E.InnerJoin` lecturer) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do ldapAdminUsers <- fmap (map E.unValue) . runDB . E.select . E.from $ \(user `E.InnerJoin` lecturer) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
@ -109,8 +104,8 @@ ldapAdmins = do
_other -> return Nothing _other -> return Nothing
smtpConnect :: Handler (Maybe Bool) dispatchHealthCheckSMTPConnect :: Handler HealthReport
smtpConnect = do dispatchHealthCheckSMTPConnect = HealthSMTPConnect <$> do
smtpPool <- getsYesod appSmtpPool smtpPool <- getsYesod appSmtpPool
for smtpPool . flip withResource $ \smtpConn -> do for smtpPool . flip withResource $ \smtpConn -> do
response@(rCode, _) <- liftIO $ SMTP.sendCommand smtpConn SMTP.NOOP response@(rCode, _) <- liftIO $ SMTP.sendCommand smtpConn SMTP.NOOP
@ -121,8 +116,8 @@ smtpConnect = do
return False return False
widgetMemcached :: Handler (Maybe Bool) dispatchHealthCheckWidgetMemcached :: Handler HealthReport
widgetMemcached = do dispatchHealthCheckWidgetMemcached = HealthWidgetMemcached <$> do
memcachedConn <- getsYesod appWidgetMemcached memcachedConn <- getsYesod appWidgetMemcached
for memcachedConn $ \_memcachedConn' -> do for memcachedConn $ \_memcachedConn' -> do
let ext = "bin" let ext = "bin"

View File

@ -69,7 +69,7 @@ data JobCtl = JobCtlFlush
| JobCtlPerform QueuedJobId | JobCtlPerform QueuedJobId
| JobCtlDetermineCrontab | JobCtlDetermineCrontab
| JobCtlQueue Job | JobCtlQueue Job
| JobCtlGenerateHealthReport | JobCtlGenerateHealthReport HealthCheck
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance Hashable JobCtl instance Hashable JobCtl

View File

@ -35,7 +35,9 @@ module Mail
, _partType, _partEncoding, _partFilename, _partHeaders, _partContent , _partType, _partEncoding, _partFilename, _partHeaders, _partContent
) where ) where
import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender) import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender, derivePersistFieldJSON)
import Model.Types.TH.JSON
import Network.Mail.Mime hiding (addPart, addAttachment) import Network.Mail.Mime hiding (addPart, addAttachment)
import qualified Network.Mail.Mime as Mime (addPart) import qualified Network.Mail.Mime as Mime (addPart)
@ -159,6 +161,7 @@ instance Default MailLanguages where
instance Hashable MailLanguages instance Hashable MailLanguages
data MailContext = MailContext data MailContext = MailContext
{ mcLanguages :: MailLanguages { mcLanguages :: MailLanguages
, mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat , mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat
@ -506,3 +509,6 @@ setMailSmtpData = do
in tell $ mempty { smtpEnvelopeFrom = Last $ Just verp } in tell $ mempty { smtpEnvelopeFrom = Last $ Just verp }
| otherwise | otherwise
-> tell $ mempty { smtpEnvelopeFrom = Last $ Just from } -> tell $ mempty { smtpEnvelopeFrom = Last $ Just from }
derivePersistFieldJSON ''MailLanguages

View File

@ -6,7 +6,7 @@ module Model
, module Cron.Types , module Cron.Types
) where ) where
import ClassyPrelude.Yesod import Import.NoModel
import Database.Persist.Quasi import Database.Persist.Quasi
import Database.Persist.TH.Directory import Database.Persist.TH.Directory
-- import Data.Time -- import Data.Time
@ -23,8 +23,6 @@ import Utils.Message (MessageStatus)
import Settings.Cluster (ClusterSettingsKey) import Settings.Cluster (ClusterSettingsKey)
import Data.Binary (Binary)
-- You can define all of your database entities in the entities file. -- You can define all of your database entities in the entities file.
-- You can find more information on persistent and how to declare entities -- You can find more information on persistent and how to declare entities
-- at: -- at:
@ -38,9 +36,5 @@ deriving instance Eq (Unique Sheet) -- instance Eq CourseSheet
deriving instance Eq (Unique Material) -- instance Eq UniqueMaterial deriving instance Eq (Unique Material) -- instance Eq UniqueMaterial
deriving instance Eq (Unique Tutorial) -- instance Eq Tutorial deriving instance Eq (Unique Tutorial) -- instance Eq Tutorial
-- Primary keys mentioned in dbtable row-keys must be Binary
-- Automatically generated (i.e. numeric) ids are already taken care of
deriving instance Binary (Key Term)
submissionRatingDone :: Submission -> Bool submissionRatingDone :: Submission -> Bool
submissionRatingDone Submission{..} = isJust submissionRatingTime submissionRatingDone Submission{..} = isJust submissionRatingTime

View File

@ -7,7 +7,7 @@ import Data.Aeson.TH (deriveJSON, defaultOptions)
import Utils.PathPiece import Utils.PathPiece
import qualified Model as Current import qualified Model as Current
import qualified Model.Types.JSON as Current import qualified Model.Types.TH.JSON as Current
import Data.Universe import Data.Universe

View File

@ -1,72 +1,14 @@
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} -- for instance PathPiece (CI Text)
module Model.Types module Model.Types
( module Model.Types ( module Types
, module Model.Types.Sheet
, module Model.Types.DateTime
, module Model.Types.Security
, module Model.Types.Misc
, module Numeric.Natural
, module Mail
, module Utils.DateTime
, module Data.UUID.Types
) where ) where
import ClassyPrelude import Model.Types.Common as Types
import Data.UUID.Types (UUID) import Model.Types.Course as Types
import qualified Data.UUID.Types as UUID import Model.Types.DateTime as Types
import Data.NonNull.Instances () import Model.Types.Exam as Types
import Model.Types.Health as Types
import Data.Text (Text) import Model.Types.Mail as Types
import qualified Data.Text as Text import Model.Types.Security as Types
import Data.CaseInsensitive (CI) import Model.Types.Sheet as Types
import Data.CaseInsensitive.Instances () import Model.Types.Submission as Types
import Model.Types.Misc as Types
import Data.Universe.Instances.Reverse ()
import Yesod.Core.Dispatch (PathPiece(..))
import qualified Yesod.Auth.Util.PasswordStore as PWStore
import Web.PathPieces
import Mail (MailLanguages(..))
import Utils.DateTime (DateTimeFormat(..), SelDateTimeFormat(..))
import Numeric.Natural
import Model.Types.Sheet
import Model.Types.DateTime
import Model.Types.Security
import Model.Types.Misc
----
-- Just bringing together the different Model.Types submodules.
instance PathPiece UUID where
fromPathPiece = UUID.fromString . unpack
toPathPiece = pack . UUID.toString
instance {-# OVERLAPS #-} PathMultiPiece FilePath where
fromPathMultiPiece = Just . unpack . intercalate "/"
toPathMultiPiece = Text.splitOn "/" . pack
-- Type synonyms
type Email = Text
type SchoolName = CI Text
type SchoolShorthand = CI Text
type CourseName = CI Text
type CourseShorthand = CI Text
type SheetName = CI Text
type MaterialName = CI Text
type UserEmail = CI Email
type TutorialName = CI Text
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
type InstanceId = UUID
type ClusterId = UUID
type TokenId = UUID
type TermCandidateIncidence = UUID

35
src/Model/Types/Common.hs Normal file
View File

@ -0,0 +1,35 @@
{-|
Module: Model.Types.Common
Description: Common types used by most @Model.Types.*@-Modules
Types used by multiple other @Model.Types.*@-Modules
-}
module Model.Types.Common
( module Model.Types.Common
) where
import Import.NoModel
import qualified Yesod.Auth.Util.PasswordStore as PWStore
type Count = Sum Integer
type Points = Centi
type Email = Text
type SchoolName = CI Text
type SchoolShorthand = CI Text
type CourseName = CI Text
type CourseShorthand = CI Text
type SheetName = CI Text
type MaterialName = CI Text
type UserEmail = CI Email
type TutorialName = CI Text
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
type InstanceId = UUID
type ClusterId = UUID
type TokenId = UUID
type TermCandidateIncidence = UUID

26
src/Model/Types/Course.hs Normal file
View File

@ -0,0 +1,26 @@
{-|
Module: Model.Types.Course
Description: Types for modeling Courses
Also see `Model.Types.Sheet`
-}
module Model.Types.Course
( module Model.Types.Course
) where
import Import.NoModel
data LecturerType = CourseLecturer | CourseAssistant
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe LecturerType
instance Finite LecturerType
nullaryPathPiece ''LecturerType $ camelToPathPiece' 1
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
} ''LecturerType
derivePersistFieldJSON ''LecturerType
instance Hashable LecturerType

View File

@ -1,34 +1,28 @@
{-# LANGUAGE GeneralizedNewtypeDeriving {-|
, UndecidableInstances Module: Model.Types.DateTime
#-} Description: Time related types
{-# OPTIONS_GHC -fno-warn-orphans #-} -- for instance PathPiece (CI Text)
module Model.Types.DateTime where Terms, Seasons, and Occurence schedules
-}
module Model.Types.DateTime
( module Model.Types.DateTime
) where
import Import.NoModel
import ClassyPrelude
import GHC.Generics (Generic)
import Utils
import Control.Lens import Control.Lens
import Data.NonNull.Instances ()
import Data.Typeable (Typeable)
import Data.Universe.Instances.Reverse ()
import Data.Binary (Binary)
import Data.Text (Text)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Data.CaseInsensitive.Instances ()
import Text.Read (readMaybe) import Text.Read (readMaybe)
import Database.Persist.Class
import Database.Persist.Sql import Database.Persist.Sql
import Web.HttpApiData import Web.HttpApiData
import Yesod.Core.Dispatch (PathPiece(..)) import Data.Aeson.Types as Aeson
import qualified Data.Aeson as Aeson
import Data.Aeson (FromJSON(..), ToJSON(..), withText) import Time.Types (WeekDay(..))
import Data.Time.LocalTime (LocalTime, TimeOfDay)
---- ----
@ -70,6 +64,7 @@ instance Enum TermIdentifier where
-- from_TermIdentifier_to_TermId = TermKey -- from_TermIdentifier_to_TermId = TermKey
shortened :: Iso' Integer Integer shortened :: Iso' Integer Integer
-- ^ Year numbers shortened to two digits
shortened = iso shorten expand shortened = iso shorten expand
where where
century = ($currentYear `div` 100) * 100 century = ($currentYear `div` 100) * 100
@ -156,3 +151,44 @@ time `withinTerm` term = timeYear `mod` 100 == termYear `mod` 100
timeYear = fst3 $ toGregorian time timeYear = fst3 $ toGregorian time
termYear = year term termYear = year term
data OccurenceSchedule = ScheduleWeekly
{ scheduleDayOfWeek :: WeekDay
, scheduleStart :: TimeOfDay
, scheduleEnd :: TimeOfDay
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
, constructorTagModifier = camelToPathPiece' 1
, tagSingleConstructors = True
, sumEncoding = TaggedObject "repeat" "schedule"
} ''OccurenceSchedule
data OccurenceException = ExceptOccur
{ exceptDay :: Day
, exceptStart :: TimeOfDay
, exceptEnd :: TimeOfDay
}
| ExceptNoOccur
{ exceptTime :: LocalTime
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
, constructorTagModifier = camelToPathPiece' 1
, sumEncoding = TaggedObject "exception" "for"
} ''OccurenceException
data Occurences = Occurences
{ occurencesScheduled :: Set OccurenceSchedule
, occurencesExceptions :: Set OccurenceException
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''Occurences
derivePersistFieldJSON ''Occurences

16
src/Model/Types/Exam.hs Normal file
View File

@ -0,0 +1,16 @@
{-|
Module: Model.Types.Exam
Description: Types for modeling Exams
-}
module Model.Types.Exam
( module Model.Types.Exam
) where
import Import.NoModel
import Database.Persist.TH (derivePersistField)
data ExamStatus = Attended | NoShow | Voided
deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic)
derivePersistField "ExamStatus"

87
src/Model/Types/Health.hs Normal file
View File

@ -0,0 +1,87 @@
{-|
Module: Model.Types.Health
Description: Types for running self-tests
-}
module Model.Types.Health
( module Model.Types.Health
) where
import Import.NoModel
data HealthCheck
= HealthCheckMatchingClusterConfig
| HealthCheckHTTPReachable
| HealthCheckLDAPAdmins
| HealthCheckSMTPConnect
| HealthCheckWidgetMemcached
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe HealthCheck
instance Finite HealthCheck
instance Hashable HealthCheck
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 2
} ''HealthCheck
nullaryPathPiece ''HealthCheck $ camelToPathPiece' 2
pathPieceJSONKey ''HealthCheck
data HealthReport
= HealthMatchingClusterConfig { healthMatchingClusterConfig :: Bool }
-- ^ Is the database-stored configuration we're running under still up to date?
--
-- Also tests database connection as a side effect
| HealthHTTPReachable { healthHTTPReachable :: Maybe Bool }
-- ^ Can we reach a uni2work-instance with the same `ClusterId` under our configured `approot` via HTTP?
| HealthLDAPAdmins { healthLDAPAdmins :: Maybe Rational }
-- ^ Proportion of school admins that could be found in LDAP
| HealthSMTPConnect { healthSMTPConnect :: Maybe Bool }
-- ^ Can we connect to the SMTP server and say @NOOP@?
| HealthWidgetMemcached { healthWidgetMemcached :: Maybe Bool }
-- ^ Can we store values in memcached and retrieve them via HTTP?
deriving (Eq, Ord, Read, Show, Data, Generic, Typeable)
instance NFData HealthReport
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
, fieldLabelModifier = camelToPathPiece' 1
, omitNothingFields = True
, sumEncoding = TaggedObject "test" "result"
, tagSingleConstructors = True
} ''HealthReport
classifyHealthReport :: HealthReport -> HealthCheck
classifyHealthReport HealthMatchingClusterConfig{} = HealthCheckMatchingClusterConfig
classifyHealthReport HealthLDAPAdmins{} = HealthCheckLDAPAdmins
classifyHealthReport HealthHTTPReachable{} = HealthCheckHTTPReachable
classifyHealthReport HealthSMTPConnect{} = HealthCheckSMTPConnect
classifyHealthReport HealthWidgetMemcached{} = HealthCheckWidgetMemcached
-- | `HealthReport` classified (`classifyHealthReport`) by badness
--
-- > a < b = a `worseThan` b
--
-- Currently all consumers of this type check for @(== HealthSuccess)@; this
-- needs to be adjusted on a case-by-case basis if new constructors are added
data HealthStatus = HealthFailure | HealthSuccess
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe HealthStatus
instance Finite HealthStatus
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
} ''HealthStatus
nullaryPathPiece ''HealthStatus $ camelToPathPiece' 1
healthReportStatus :: HealthReport -> HealthStatus
-- ^ Classify `HealthReport` by badness
healthReportStatus = \case
HealthMatchingClusterConfig False -> HealthFailure
HealthHTTPReachable (Just False) -> HealthFailure
HealthLDAPAdmins (Just prop )
| prop <= 0 -> HealthFailure
HealthSMTPConnect (Just False) -> HealthFailure
HealthWidgetMemcached (Just False) -> HealthFailure -- TODO: investigate this failure mode; do we just handle it gracefully?
_other -> maxBound -- Minimum badness

75
src/Model/Types/Mail.hs Normal file
View File

@ -0,0 +1,75 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
{-|
Module: Model.Types.Mail
Description: Types related to Notifications
-}
module Model.Types.Mail
( module Model.Types.Mail
) where
import Import.NoModel
import qualified Data.Aeson.Types as Aeson
import qualified Data.HashMap.Strict as HashMap
-- ^ `NotificationSettings` is for now a series of boolean checkboxes, i.e. a mapping @NotificationTrigger -> Bool@
--
-- Could maybe be replaced with `Structure Notification` in the long term
data NotificationTrigger
= NTSubmissionRatedGraded
| NTSubmissionRated
| NTSheetActive
| NTSheetSoonInactive
| NTSheetInactive
| NTCorrectionsAssigned
| NTCorrectionsNotDistributed
| NTUserRightsUpdate
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe NotificationTrigger
instance Finite NotificationTrigger
instance Hashable NotificationTrigger
deriveJSON defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 1 . splitCamel
} ''NotificationTrigger
instance ToJSONKey NotificationTrigger where
toJSONKey = toJSONKeyText $ \v -> let Aeson.String t = toJSON v in t
instance FromJSONKey NotificationTrigger where
fromJSONKey = FromJSONKeyTextParser $ parseJSON . Aeson.String
newtype NotificationSettings = NotificationSettings { notificationAllowed :: NotificationTrigger -> Bool }
deriving (Generic, Typeable)
deriving newtype (Eq, Ord, Read, Show)
instance Default NotificationSettings where
def = NotificationSettings $ \case
NTSubmissionRatedGraded -> True
NTSubmissionRated -> False
NTSheetActive -> True
NTSheetSoonInactive -> False
NTSheetInactive -> True
NTCorrectionsAssigned -> True
NTCorrectionsNotDistributed -> True
NTUserRightsUpdate -> True
instance ToJSON NotificationSettings where
toJSON v = toJSON . HashMap.fromList $ map (id &&& notificationAllowed v) universeF
instance FromJSON NotificationSettings where
parseJSON = Aeson.withObject "NotificationSettings" $ \o -> do
o' <- parseJSON $ Aeson.Object o :: Aeson.Parser (HashMap NotificationTrigger Bool)
return . NotificationSettings $ \n -> case HashMap.lookup n o' of
Nothing -> notificationAllowed def n
Just b -> b
derivePersistFieldJSON ''NotificationSettings

View File

@ -1,50 +1,25 @@
{-# LANGUAGE GeneralizedNewtypeDeriving {-|
, UndecidableInstances Module: Model.Types.Misc
#-} Description: Additional uncategorized types
{-# OPTIONS_GHC -fno-warn-orphans #-} -- for instance PathPiece (CI Text) -}
module Model.Types.Misc where module Model.Types.Misc
( module Model.Types.Misc
) where
import Import.NoModel
import ClassyPrelude
import Utils
import Control.Lens import Control.Lens
import Data.NonNull.Instances ()
import Data.Set (Set)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.Universe
import Data.Universe.Helpers
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Lens as Text import qualified Data.Text.Lens as Text
import Data.CaseInsensitive.Instances ()
import Database.Persist.TH hiding (derivePersistFieldJSON)
import Model.Types.JSON
import Data.Aeson (Value())
import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..), SumEncoding(..))
import GHC.Generics (Generic)
import Data.Typeable (Typeable)
import Data.Universe.Instances.Reverse ()
import Data.Time.LocalTime (LocalTime, TimeOfDay)
import Time.Types (WeekDay(..))
-----
-- Miscellaneous Model.Types
derivePersistFieldJSON ''Value
data StudyFieldType = FieldPrimary | FieldSecondary data StudyFieldType = FieldPrimary | FieldSecondary
deriving (Eq, Ord, Enum, Show, Read, Bounded, Generic) deriving (Eq, Ord, Enum, Show, Read, Bounded, Generic)
derivePersistField "StudyFieldType" derivePersistField "StudyFieldType"
-- instance DisplayAble StudyFieldType
data Theme data Theme
= ThemeDefault = ThemeDefault
@ -59,89 +34,11 @@ deriveJSON defaultOptions
{ constructorTagModifier = fromJust . stripPrefix "Theme" { constructorTagModifier = fromJust . stripPrefix "Theme"
} ''Theme } ''Theme
instance Universe Theme where universe = universeDef instance Universe Theme
instance Finite Theme instance Finite Theme
nullaryPathPiece ''Theme (camelToPathPiece' 1) nullaryPathPiece ''Theme $ camelToPathPiece' 1
$(deriveSimpleWith ''DisplayAble 'display (over Text.packed $ Text.intercalate " " . unsafeTail . splitCamel) ''Theme) -- describe theme to user $(deriveSimpleWith ''DisplayAble 'display (over Text.packed $ Text.intercalate " " . unsafeTail . splitCamel) ''Theme) -- describe theme to user
derivePersistField "Theme" derivePersistField "Theme"
data CorrectorState = CorrectorNormal | CorrectorMissing | CorrectorExcused
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
deriveJSON defaultOptions
{ constructorTagModifier = fromJust . stripPrefix "Corrector"
} ''CorrectorState
instance Universe CorrectorState
instance Finite CorrectorState
instance Hashable CorrectorState
nullaryPathPiece ''CorrectorState (camelToPathPiece' 1)
derivePersistField "CorrectorState"
data LecturerType = CourseLecturer | CourseAssistant
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe LecturerType
instance Finite LecturerType
nullaryPathPiece ''LecturerType $ camelToPathPiece' 1
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
} ''LecturerType
derivePersistFieldJSON ''LecturerType
instance Hashable LecturerType
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece
} ''WeekDay
data OccurenceSchedule = ScheduleWeekly
{ scheduleDayOfWeek :: WeekDay
, scheduleStart :: TimeOfDay
, scheduleEnd :: TimeOfDay
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
, constructorTagModifier = camelToPathPiece' 1
, tagSingleConstructors = True
, sumEncoding = TaggedObject "repeat" "schedule"
} ''OccurenceSchedule
data OccurenceException = ExceptOccur
{ exceptDay :: Day
, exceptStart :: TimeOfDay
, exceptEnd :: TimeOfDay
}
| ExceptNoOccur
{ exceptTime :: LocalTime
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
, constructorTagModifier = camelToPathPiece' 1
, sumEncoding = TaggedObject "exception" "for"
} ''OccurenceException
data Occurences = Occurences
{ occurencesScheduled :: Set OccurenceSchedule
, occurencesExceptions :: Set OccurenceException
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''Occurences
derivePersistFieldJSON ''Occurences

View File

@ -1,83 +1,26 @@
{-# LANGUAGE GeneralizedNewtypeDeriving {-# LANGUAGE GeneralizedNewtypeDeriving #-}
, UndecidableInstances
#-}
{-# OPTIONS_GHC -fno-warn-orphans #-} -- for instance PathPiece (CI Text)
module Model.Types.Security where {-|
Module: Model.Types.Security
Description: Types for authentication and authorisation
-}
module Model.Types.Security
( module Model.Types.Security
) where
import ClassyPrelude import Import.NoModel
import Utils
import Control.Lens hiding (universe)
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set
import Data.Universe
import Data.UUID.Types (UUID)
import qualified Data.UUID.Types as UUID
import Data.NonNull.Instances ()
import Data.Default
import Model.Types.JSON
import Database.Persist.Class
import Database.Persist.Sql
import Data.Text (Text)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Data.CaseInsensitive.Instances ()
import Yesod.Core.Dispatch (PathPiece(..))
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson import qualified Data.Aeson.Types as Aeson
import Data.Aeson (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), FromJSONKeyFunction(..), withObject)
import Data.Aeson.Types (toJSONKeyText)
import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..), SumEncoding(..), mkToJSON, mkParseJSON)
import GHC.Generics (Generic)
import Data.Typeable (Typeable)
import Data.Universe.Instances.Reverse ()
import Mail (MailLanguages(..))
import Data.Word.Word24 (Word24)
import Data.Bits
import Data.Ix
import Data.List (genericIndex, elemIndex)
import System.Random (Random(..))
import Data.Data (Data)
import Model.Types.Wordlist
import Data.Text.Metrics (damerauLevenshtein)
import Data.Binary (Binary)
import qualified Data.Binary as Binary import qualified Data.Binary as Binary
import Data.Semigroup (Min(..))
import Control.Monad.Trans.Writer (execWriter)
import Control.Monad.Writer.Class (MonadWriter(..))
----
-- Security, Authentification, Notification Stuff
instance PersistField UUID where
toPersistValue = PersistDbSpecific . UUID.toASCIIBytes
fromPersistValue (PersistText t) = maybe (Left "Failed to parse UUID") Right $ UUID.fromText t
fromPersistValue (PersistByteString bs) = maybe (Left "Failed to parse UUID") Right $ UUID.fromASCIIBytes bs
fromPersistValue (PersistDbSpecific bs) = maybe (Left "Failed to parse UUID") Right $ UUID.fromASCIIBytes bs
fromPersistValue x = Left $ "Expected UUID, received: " <> tshow x
instance PersistFieldSql UUID where
sqlType _ = SqlOther "uuid"
data AuthenticationMode = AuthLDAP data AuthenticationMode = AuthLDAP
| AuthPWHash { authPWHash :: Text } | AuthPWHash { authPWHash :: Text }
@ -92,167 +35,6 @@ deriveJSON defaultOptions
derivePersistFieldJSON ''AuthenticationMode derivePersistFieldJSON ''AuthenticationMode
-- ^ `NotificationSettings` is for now a series of boolean checkboxes, i.e. a mapping @NotificationTrigger -> Bool@
--
-- Could maybe be replaced with `Structure Notification` in the long term
data NotificationTrigger = NTSubmissionRatedGraded
| NTSubmissionRated
| NTSheetActive
| NTSheetSoonInactive
| NTSheetInactive
| NTCorrectionsAssigned
| NTCorrectionsNotDistributed
| NTUserRightsUpdate
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe NotificationTrigger
instance Finite NotificationTrigger
instance Hashable NotificationTrigger
deriveJSON defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 1 . splitCamel
} ''NotificationTrigger
instance ToJSONKey NotificationTrigger where
toJSONKey = toJSONKeyText $ \v -> let Aeson.String t = toJSON v in t
instance FromJSONKey NotificationTrigger where
fromJSONKey = FromJSONKeyTextParser $ parseJSON . Aeson.String
newtype NotificationSettings = NotificationSettings { notificationAllowed :: NotificationTrigger -> Bool }
deriving (Generic, Typeable)
deriving newtype (Eq, Ord, Read, Show)
instance Default NotificationSettings where
def = NotificationSettings $ \case
NTSubmissionRatedGraded -> True
NTSubmissionRated -> False
NTSheetActive -> True
NTSheetSoonInactive -> False
NTSheetInactive -> True
NTCorrectionsAssigned -> True
NTCorrectionsNotDistributed -> True
NTUserRightsUpdate -> True
instance ToJSON NotificationSettings where
toJSON v = toJSON . HashMap.fromList $ map (id &&& notificationAllowed v) universeF
instance FromJSON NotificationSettings where
parseJSON = withObject "NotificationSettings" $ \o -> do
o' <- parseJSON $ Aeson.Object o :: Aeson.Parser (HashMap NotificationTrigger Bool)
return . NotificationSettings $ \n -> case HashMap.lookup n o' of
Nothing -> notificationAllowed def n
Just b -> b
derivePersistFieldJSON ''NotificationSettings
instance ToBackendKey SqlBackend record => Hashable (Key record) where
hashWithSalt s key = s `hashWithSalt` fromSqlKey key
derivePersistFieldJSON ''MailLanguages
type PseudonymWord = CI Text
newtype Pseudonym = Pseudonym Word24
deriving (Eq, Ord, Read, Show, Generic, Data)
deriving newtype (Bounded, Enum, Integral, Num, Real, Ix)
instance PersistField Pseudonym where
toPersistValue p = toPersistValue (fromIntegral p :: Word32)
fromPersistValue v = do
w <- fromPersistValue v :: Either Text Word32
if
| 0 <= w
, w <= fromIntegral (maxBound :: Pseudonym)
-> return $ fromIntegral w
| otherwise
-> Left "Pseudonym out of range"
instance PersistFieldSql Pseudonym where
sqlType _ = SqlInt32
instance Random Pseudonym where
randomR (max minBound -> lo, min maxBound -> hi) gen = over _1 (fromIntegral :: Word32 -> Pseudonym) $ randomR (fromIntegral lo, fromIntegral hi) gen
random = randomR (minBound, maxBound)
instance FromJSON Pseudonym where
parseJSON v@(Aeson.Number _) = do
w <- parseJSON v :: Aeson.Parser Word32
if
| 0 <= w
, w <= fromIntegral (maxBound :: Pseudonym)
-> return $ fromIntegral w
| otherwise
-> fail "Pseudonym out auf range"
parseJSON (Aeson.String t)
= case t ^? _PseudonymText of
Just p -> return p
Nothing -> fail "Could not parse pseudonym"
parseJSON v = flip (Aeson.withArray "Pseudonym") v $ \ws -> do
ws' <- toList . map CI.mk <$> mapM parseJSON ws
case ws' ^? _PseudonymWords of
Just p -> return p
Nothing -> fail "Could not parse pseudonym words"
instance ToJSON Pseudonym where
toJSON = toJSON . (review _PseudonymWords :: Pseudonym -> [PseudonymWord])
pseudonymWordlist :: [PseudonymWord]
pseudonymCharacters :: Set (CI Char)
(pseudonymWordlist, pseudonymCharacters) = $(wordlist "config/wordlist.txt")
_PseudonymWords :: Prism' [PseudonymWord] Pseudonym
_PseudonymWords = prism' pToWords pFromWords
where
pFromWords :: [PseudonymWord] -> Maybe Pseudonym
pFromWords [w1, w2]
| Just i1 <- elemIndex w1 pseudonymWordlist
, Just i2 <- elemIndex w2 pseudonymWordlist
, i1 <= maxWord, i2 <= maxWord
= Just . Pseudonym $ shiftL (fromIntegral i1) 12 .|. fromIntegral i2
pFromWords _ = Nothing
pToWords :: Pseudonym -> [PseudonymWord]
pToWords (Pseudonym p)
= [ genericIndex pseudonymWordlist $ shiftR p 12 .&. maxWord
, genericIndex pseudonymWordlist $ p .&. maxWord
]
maxWord :: Num a => a
maxWord = 0b111111111111
_PseudonymText :: Prism' Text Pseudonym
_PseudonymText = prism' tToWords tFromWords . _PseudonymWords
where
tFromWords :: Text -> Maybe [PseudonymWord]
tFromWords input
| [result] <- input ^.. pseudonymFragments
= Just result
| otherwise
= Nothing
tToWords :: [PseudonymWord] -> Text
tToWords = Text.unwords . map CI.original
pseudonymWords :: Fold Text PseudonymWord
pseudonymWords = folding
$ \(CI.mk -> input) -> map (view _2) . fromMaybe [] . listToMaybe . groupBy ((==) `on` view _1) . sortBy (comparing $ view _1) . filter ((<= distanceCutoff) . view _1) $ map (distance input &&& id) pseudonymWordlist
where
distance = damerauLevenshtein `on` CI.foldedCase
-- | Arbitrary cutoff point, for reference: ispell cuts off at 1
distanceCutoff = 2
pseudonymFragments :: Fold Text [PseudonymWord]
pseudonymFragments = folding
$ mapM (toListOf pseudonymWords) . (\l -> guard (length l == 2) *> l) . filter (not . null) . Text.split (\(CI.mk -> c) -> not $ Set.member c pseudonymCharacters)
data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prädikate sind sortier nach Relevanz für Benutzer data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prädikate sind sortier nach Relevanz für Benutzer
= AuthAdmin = AuthAdmin
| AuthLecturer | AuthLecturer
@ -313,7 +95,7 @@ instance ToJSON AuthTagActive where
toJSON v = toJSON . HashMap.fromList $ map (id &&& authTagIsActive v) universeF toJSON v = toJSON . HashMap.fromList $ map (id &&& authTagIsActive v) universeF
instance FromJSON AuthTagActive where instance FromJSON AuthTagActive where
parseJSON = withObject "AuthTagActive" $ \o -> do parseJSON = Aeson.withObject "AuthTagActive" $ \o -> do
o' <- parseJSON $ Aeson.Object o :: Aeson.Parser (HashMap AuthTag Bool) o' <- parseJSON $ Aeson.Object o :: Aeson.Parser (HashMap AuthTag Bool)
return . AuthTagActive $ \n -> case HashMap.lookup n o' of return . AuthTagActive $ \n -> case HashMap.lookup n o' of
Nothing -> authTagIsActive def n Nothing -> authTagIsActive def n
@ -359,53 +141,3 @@ instance (Ord a, Binary a) => Binary (PredDNF a) where
type AuthLiteral = PredLiteral AuthTag type AuthLiteral = PredLiteral AuthTag
type AuthDNF = PredDNF AuthTag type AuthDNF = PredDNF AuthTag
data HealthReport = HealthReport
{ healthMatchingClusterConfig :: Bool
-- ^ Is the database-stored configuration we're running under still up to date?
, healthHTTPReachable :: Maybe Bool
-- ^ Can we reach a uni2work-instance with the same `ClusterId` under our configured `approot` via HTTP?
--
-- Can be `Nothing` if we don't have a static configuration setting `appRoot` or if check is disabled in settings
, healthLDAPAdmins :: Maybe Rational
-- ^ Proportion of school admins that could be found in LDAP
--
-- Is `Nothing` if LDAP is not configured or no users are school admins
, healthSMTPConnect :: Maybe Bool
-- ^ Can we connect to the SMTP server and say @NOOP@?
, healthWidgetMemcached :: Maybe Bool
-- ^ Can we store values in memcached and retrieve them via HTTP?
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
, omitNothingFields = True
} ''HealthReport
-- | `HealthReport` classified (`classifyHealthReport`) by badness
--
-- > a < b = a `worseThan` b
--
-- Currently all consumers of this type check for @(== HealthSuccess)@; this
-- needs to be adjusted on a case-by-case basis if new constructors are added
data HealthStatus = HealthFailure | HealthSuccess
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe HealthStatus
instance Finite HealthStatus
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
} ''HealthStatus
nullaryPathPiece ''HealthStatus $ camelToPathPiece' 1
classifyHealthReport :: HealthReport -> HealthStatus
-- ^ Classify `HealthReport` by badness
classifyHealthReport HealthReport{..} = getMin . execWriter $ do -- Construction with `Writer (Min HealthStatus) a` returns worst `HealthStatus` passed to `tell` at any point
unless healthMatchingClusterConfig . tell $ Min HealthFailure
unless (fromMaybe True healthHTTPReachable) . tell $ Min HealthFailure
unless (maybe True (> 0) healthLDAPAdmins) . tell $ Min HealthFailure
unless (fromMaybe True healthSMTPConnect) . tell $ Min HealthFailure
unless (fromMaybe True healthWidgetMemcached) . tell $ Min HealthFailure

View File

@ -1,62 +1,31 @@
{-# LANGUAGE GeneralizedNewtypeDeriving {-|
, UndecidableInstances Module: Model.Types.Sheet
#-} Description: Types for modeling sheets
{-# OPTIONS_GHC -fno-warn-orphans #-} -- for instance PathPiece (CI Text) -}
module Model.Types.Sheet where module Model.Types.Sheet
( module Model.Types.Sheet
) where
import ClassyPrelude import Import.NoModel
import Utils import Model.Types.Common
import Numeric.Natural import Utils.Lens.TH
import Control.Lens import Control.Lens
import Utils.Lens.TH
import GHC.Generics (Generic)
import Generics.Deriving.Monoid (memptydefault, mappenddefault) import Generics.Deriving.Monoid (memptydefault, mappenddefault)
import Data.Typeable (Typeable)
import Data.Universe
import Data.Universe.Helpers
import Data.Universe.Instances.Reverse ()
import Data.NonNull.Instances ()
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Fixed
import Data.Monoid (Sum(..))
import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..), SumEncoding(..)) import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..), SumEncoding(..))
import Data.CaseInsensitive.Instances ()
import Text.Blaze (Markup) import Text.Blaze (Markup)
import Database.Persist.TH hiding (derivePersistFieldJSON)
import Model.Types.JSON
import Yesod.Core.Dispatch (PathPiece(..)) import Yesod.Core.Dispatch (PathPiece(..))
import Network.Mime import Data.Maybe (fromJust)
----
-- Sheet and Submission realted Model.Types
type Count = Sum Integer
type Points = Centi
toPoints :: Integral a => a -> Points -- deprecated
toPoints = fromIntegral
pToI :: Points -> Integer -- deprecated
pToI = fromPoints
fromPoints :: Integral a => Points -> a -- deprecated
fromPoints = round
instance DisplayAble Points
instance DisplayAble a => DisplayAble (Sum a) where
display (Sum x) = display x
data SheetGrading data SheetGrading
= Points { maxPoints :: Points } = Points { maxPoints :: Points }
| PassPoints { maxPoints, passingPoints :: Points } | PassPoints { maxPoints, passingPoints :: Points }
@ -179,7 +148,7 @@ data SheetFileType = SheetExercise | SheetHint | SheetSolution | SheetMarking
deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic) deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic)
derivePersistField "SheetFileType" derivePersistField "SheetFileType"
instance Universe SheetFileType where universe = universeDef instance Universe SheetFileType
instance Finite SheetFileType instance Finite SheetFileType
instance PathPiece SheetFileType where instance PathPiece SheetFileType where
@ -208,22 +177,6 @@ sheetFile2markup SheetMarking = iconMarking
partitionFileType :: Ord a => [(SheetFileType,a)] -> SheetFileType -> Set a partitionFileType :: Ord a => [(SheetFileType,a)] -> SheetFileType -> Set a
partitionFileType fs t = Map.findWithDefault Set.empty t . Map.fromListWith Set.union $ map (over _2 Set.singleton) fs partitionFileType fs t = Map.findWithDefault Set.empty t . Map.fromListWith Set.union $ map (over _2 Set.singleton) fs
data SubmissionFileType = SubmissionOriginal | SubmissionCorrected
deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic)
instance Universe SubmissionFileType
instance Finite SubmissionFileType
nullaryPathPiece ''SubmissionFileType $ camelToPathPiece' 1
submissionFileTypeIsUpdate :: SubmissionFileType -> Bool
submissionFileTypeIsUpdate SubmissionOriginal = False
submissionFileTypeIsUpdate SubmissionCorrected = True
isUpdateSubmissionFileType :: Bool -> SubmissionFileType
isUpdateSubmissionFileType False = SubmissionOriginal
isUpdateSubmissionFileType True = SubmissionCorrected
data UploadSpecificFile = UploadSpecificFile data UploadSpecificFile = UploadSpecificFile
{ specificFileLabel :: Text { specificFileLabel :: Text
@ -306,10 +259,6 @@ classifySubmissionMode (SubmissionMode False (Just _)) = SubmissionModeUser
classifySubmissionMode (SubmissionMode True (Just _)) = SubmissionModeBoth classifySubmissionMode (SubmissionMode True (Just _)) = SubmissionModeBoth
data ExamStatus = Attended | NoShow | Voided
deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic)
derivePersistField "ExamStatus"
-- | Specify a corrector's workload -- | Specify a corrector's workload
data Load -- = ByTutorial { countsToLoad :: Bool } | ByProportion { load :: Rational } data Load -- = ByTutorial { countsToLoad :: Bool } | ByProportion { load :: Rational }
= Load { byTutorial :: Maybe Bool -- ^ @Just@ all from Tutorial, @True@ if counting towards overall workload = Load { byTutorial :: Maybe Bool -- ^ @Just@ all from Tutorial, @True@ if counting towards overall workload
@ -340,3 +289,19 @@ instance Monoid Load where
isByTutorial (ByTutorial {}) = True isByTutorial (ByTutorial {}) = True
isByTutorial _ = False isByTutorial _ = False
-} -}
data CorrectorState = CorrectorNormal | CorrectorMissing | CorrectorExcused
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
deriveJSON defaultOptions
{ constructorTagModifier = fromJust . stripPrefix "Corrector"
} ''CorrectorState
instance Universe CorrectorState
instance Finite CorrectorState
instance Hashable CorrectorState
nullaryPathPiece ''CorrectorState (camelToPathPiece' 1)
derivePersistField "CorrectorState"

View File

@ -0,0 +1,151 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-|
Module: Model.Types.Submission
Description: Types to support sheet submissions
-}
module Model.Types.Submission
( module Model.Types.Submission
) where
import Import.NoModel
import Data.Aeson.Types (ToJSON(..), FromJSON(..))
import qualified Data.Aeson.Types as Aeson
import Database.Persist.Sql
import Data.Word.Word24
import qualified Data.CaseInsensitive as CI
import Control.Lens
import qualified Data.Text as Text
import qualified Data.Set as Set
import Data.List (elemIndex, genericIndex)
import Data.Bits
import Data.Text.Metrics (damerauLevenshtein)
-------------------------
-- Submission Download --
-------------------------
data SubmissionFileType = SubmissionOriginal | SubmissionCorrected
deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic)
instance Universe SubmissionFileType
instance Finite SubmissionFileType
nullaryPathPiece ''SubmissionFileType $ camelToPathPiece' 1
submissionFileTypeIsUpdate :: SubmissionFileType -> Bool
submissionFileTypeIsUpdate SubmissionOriginal = False
submissionFileTypeIsUpdate SubmissionCorrected = True
isUpdateSubmissionFileType :: Bool -> SubmissionFileType
isUpdateSubmissionFileType False = SubmissionOriginal
isUpdateSubmissionFileType True = SubmissionCorrected
---------------------------
-- Submission Pseudonyms --
---------------------------
type PseudonymWord = CI Text
newtype Pseudonym = Pseudonym Word24
deriving (Eq, Ord, Read, Show, Generic, Data)
deriving newtype (Bounded, Enum, Integral, Num, Real, Ix)
instance PersistField Pseudonym where
toPersistValue p = toPersistValue (fromIntegral p :: Word32)
fromPersistValue v = do
w <- fromPersistValue v :: Either Text Word32
if
| 0 <= w
, w <= fromIntegral (maxBound :: Pseudonym)
-> return $ fromIntegral w
| otherwise
-> Left "Pseudonym out of range"
instance PersistFieldSql Pseudonym where
sqlType _ = SqlInt32
instance Random Pseudonym where
randomR (max minBound -> lo, min maxBound -> hi) gen = over _1 (fromIntegral :: Word32 -> Pseudonym) $ randomR (fromIntegral lo, fromIntegral hi) gen
random = randomR (minBound, maxBound)
instance FromJSON Pseudonym where
parseJSON v@(Aeson.Number _) = do
w <- parseJSON v :: Aeson.Parser Word32
if
| 0 <= w
, w <= fromIntegral (maxBound :: Pseudonym)
-> return $ fromIntegral w
| otherwise
-> fail "Pseudonym out auf range"
parseJSON (Aeson.String t)
= case t ^? _PseudonymText of
Just p -> return p
Nothing -> fail "Could not parse pseudonym"
parseJSON v = flip (Aeson.withArray "Pseudonym") v $ \ws -> do
ws' <- toList . map CI.mk <$> mapM parseJSON ws
case ws' ^? _PseudonymWords of
Just p -> return p
Nothing -> fail "Could not parse pseudonym words"
instance ToJSON Pseudonym where
toJSON = toJSON . (review _PseudonymWords :: Pseudonym -> [PseudonymWord])
pseudonymWordlist :: [PseudonymWord]
pseudonymCharacters :: Set (CI Char)
(pseudonymWordlist, pseudonymCharacters) = $(wordlist "config/wordlist.txt")
_PseudonymWords :: Prism' [PseudonymWord] Pseudonym
_PseudonymWords = prism' pToWords pFromWords
where
pFromWords :: [PseudonymWord] -> Maybe Pseudonym
pFromWords [w1, w2]
| Just i1 <- elemIndex w1 pseudonymWordlist
, Just i2 <- elemIndex w2 pseudonymWordlist
, i1 <= maxWord, i2 <= maxWord
= Just . Pseudonym $ shiftL (fromIntegral i1) 12 .|. fromIntegral i2
pFromWords _ = Nothing
pToWords :: Pseudonym -> [PseudonymWord]
pToWords (Pseudonym p)
= [ genericIndex pseudonymWordlist $ shiftR p 12 .&. maxWord
, genericIndex pseudonymWordlist $ p .&. maxWord
]
maxWord :: Num a => a
maxWord = 0b111111111111
_PseudonymText :: Prism' Text Pseudonym
_PseudonymText = prism' tToWords tFromWords . _PseudonymWords
where
tFromWords :: Text -> Maybe [PseudonymWord]
tFromWords input
| [result] <- input ^.. pseudonymFragments
= Just result
| otherwise
= Nothing
tToWords :: [PseudonymWord] -> Text
tToWords = Text.unwords . map CI.original
pseudonymWords :: Fold Text PseudonymWord
pseudonymWords = folding
$ \(CI.mk -> input) -> map (view _2) . fromMaybe [] . listToMaybe . groupBy ((==) `on` view _1) . sortBy (comparing $ view _1) . filter ((<= distanceCutoff) . view _1) $ map (distance input &&& id) pseudonymWordlist
where
distance = damerauLevenshtein `on` CI.foldedCase
-- | Arbitrary cutoff point, for reference: ispell cuts off at 1
distanceCutoff = 2
pseudonymFragments :: Fold Text [PseudonymWord]
pseudonymFragments = folding
$ mapM (toListOf pseudonymWords) . (\l -> guard (length l == 2) *> l) . filter (not . null) . Text.split (\(CI.mk -> c) -> not $ Set.member c pseudonymCharacters)

View File

@ -1,4 +1,4 @@
module Model.Types.JSON module Model.Types.TH.JSON
( derivePersistFieldJSON ( derivePersistFieldJSON
, predNFAesonOptions , predNFAesonOptions
) where ) where

View File

@ -1,4 +1,6 @@
module Model.Types.Wordlist (wordlist) where module Model.Types.TH.Wordlist
( wordlist
) where
import ClassyPrelude hiding (lift) import ClassyPrelude hiding (lift)

View File

@ -10,14 +10,13 @@ module Settings
, module Settings.Cluster , module Settings.Cluster
) where ) where
import ClassyPrelude.Yesod import Import.NoModel
import Data.UUID (UUID) import Data.UUID (UUID)
import qualified Control.Exception as Exception import qualified Control.Exception as Exception
import Data.Aeson (Result (..), fromJSON, withObject import Data.Aeson (fromJSON, withObject
,(.!=), (.:?), withScientific ,(.!=), (.:?), withScientific
) )
import qualified Data.Aeson.Types as Aeson import qualified Data.Aeson.Types as Aeson
import Data.Aeson.TH
import Data.FileEmbed (embedFile) import Data.FileEmbed (embedFile)
import Data.Yaml (decodeEither') import Data.Yaml (decodeEither')
import Database.Persist.Postgresql (PostgresConf) import Database.Persist.Postgresql (PostgresConf)
@ -45,7 +44,6 @@ import qualified Data.Text.Encoding as Text
import qualified Ldap.Client as Ldap import qualified Ldap.Client as Ldap
import Utils hiding (MessageStatus(..))
import Control.Lens import Control.Lens
import qualified Network.HaskellNet.Auth as HaskellNet (UserName, Password, AuthType(..)) import qualified Network.HaskellNet.Auth as HaskellNet (UserName, Password, AuthType(..))
@ -70,7 +68,6 @@ import Jose.Jwt (JwtEncoding(..))
import System.FilePath.Glob import System.FilePath.Glob
import Handler.Utils.Submission.TH import Handler.Utils.Submission.TH
import Network.Mime
import Network.Mime.TH import Network.Mime.TH
import qualified Data.Map as Map import qualified Data.Map as Map
@ -118,9 +115,9 @@ data AppSettings = AppSettings
, appJwtExpiration :: Maybe NominalDiffTime , appJwtExpiration :: Maybe NominalDiffTime
, appJwtEncoding :: JwtEncoding , appJwtEncoding :: JwtEncoding
, appHealthCheckInterval :: NominalDiffTime , appHealthCheckInterval :: HealthCheck -> Maybe NominalDiffTime
, appHealthCheckHTTP :: Bool
, appHealthCheckDelayNotify :: Bool , appHealthCheckDelayNotify :: Bool
, appHealthCheckHTTP :: Bool
, appInitialLogSettings :: LogSettings , appInitialLogSettings :: LogSettings
@ -389,9 +386,9 @@ instance FromJSON AppSettings where
appJwtExpiration <- o .:? "jwt-expiration" appJwtExpiration <- o .:? "jwt-expiration"
appJwtEncoding <- o .: "jwt-encoding" appJwtEncoding <- o .: "jwt-encoding"
appHealthCheckInterval <- o .: "health-check-interval" appHealthCheckInterval <- (assertM' (> 0) . ) <$> o .: "health-check-interval"
appHealthCheckHTTP <- o .: "health-check-http"
appHealthCheckDelayNotify <- o .: "health-check-delay-notify" appHealthCheckDelayNotify <- o .: "health-check-delay-notify"
appHealthCheckHTTP <- o .: "health-check-http"
appSessionTimeout <- o .: "session-timeout" appSessionTimeout <- o .: "session-timeout"
@ -483,5 +480,5 @@ configSettingsYmlValue = either Exception.throw id
compileTimeAppSettings :: AppSettings compileTimeAppSettings :: AppSettings
compileTimeAppSettings = compileTimeAppSettings =
case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of
Error e -> error e Aeson.Error e -> error e
Success settings -> settings Aeson.Success settings -> settings

View File

@ -0,0 +1,16 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module System.FilePath.Instances
(
) where
import ClassyPrelude
import qualified Data.Text as Text
import Web.PathPieces
instance {-# OVERLAPS #-} PathMultiPiece FilePath where
fromPathMultiPiece = Just . unpack . intercalate "/"
toPathMultiPiece = Text.splitOn "/" . pack

View File

@ -12,8 +12,14 @@ import Data.Universe
import Utils.PathPiece import Utils.PathPiece
import Data.Aeson.TH
instance Universe WeekDay instance Universe WeekDay
instance Finite WeekDay instance Finite WeekDay
nullaryPathPiece ''WeekDay camelToPathPiece nullaryPathPiece ''WeekDay camelToPathPiece
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece
} ''WeekDay

View File

@ -1,5 +1,3 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} -- Monad FormResult
module Utils module Utils
( module Utils ( module Utils
) where ) where
@ -68,7 +66,7 @@ import qualified Crypto.Saltine.Core.SecretBox as SecretBox
import qualified Crypto.Saltine.Class as Saltine import qualified Crypto.Saltine.Class as Saltine
import qualified Crypto.Data.PKCS7 as PKCS7 import qualified Crypto.Data.PKCS7 as PKCS7
import Data.Fixed (Centi) import Data.Fixed
import Data.Ratio ((%)) import Data.Ratio ((%))
import qualified Data.Binary as Binary import qualified Data.Binary as Binary
@ -79,6 +77,8 @@ import Data.Time.Clock
import Data.List.NonEmpty (NonEmpty, nonEmpty) import Data.List.NonEmpty (NonEmpty, nonEmpty)
import Algebra.Lattice (top, bottom, (/\), (\/), BoundedJoinSemiLattice, BoundedMeetSemiLattice)
{-# ANN choice ("HLint: ignore Use asum" :: String) #-} {-# ANN choice ("HLint: ignore Use asum" :: String) #-}
@ -275,6 +275,12 @@ instance DisplayAble a => DisplayAble (E.Value a) where
instance DisplayAble a => DisplayAble (CI a) where instance DisplayAble a => DisplayAble (CI a) where
display = display . CI.original display = display . CI.original
instance HasResolution a => DisplayAble (Fixed a) where
display = pack . showFixed True
instance DisplayAble a => DisplayAble (Sum a) where
display = display . getSum
{- We do not want DisplayAble for every Show-Class: {- We do not want DisplayAble for every Show-Class:
We want to explicitly verify that the resulting text can be displayed to the User! We want to explicitly verify that the resulting text can be displayed to the User!
For example: UTCTime values were shown without proper format rendering! For example: UTCTime values were shown without proper format rendering!
@ -936,3 +942,13 @@ setLastModified lastModified = do
precision = 1 precision = 1
safeMethods = [ methodGet, methodHead, methodOptions ] safeMethods = [ methodGet, methodHead, methodOptions ]
--------------
-- Lattices --
--------------
foldJoin :: (MonoFoldable mono, BoundedJoinSemiLattice (Element mono)) => mono -> Element mono
foldJoin = foldr (\/) bottom
foldMeet :: (MonoFoldable mono, BoundedMeetSemiLattice (Element mono)) => mono -> Element mono
foldMeet = foldr (/\) top

View File

@ -1,5 +1,4 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Utils.DateTime module Utils.DateTime
( timeLocaleMap ( timeLocaleMap
@ -14,10 +13,9 @@ module Utils.DateTime
import ClassyPrelude.Yesod hiding (lift) import ClassyPrelude.Yesod hiding (lift)
import System.Locale.Read import System.Locale.Read
import Data.Time (TimeZone(..), TimeLocale(..)) import Data.Time (TimeLocale(..))
import Data.Time.Zones (TZ) import Data.Time.Zones (TZ)
import Data.Time.Zones.TH (includeSystemTZ) import Data.Time.Zones.TH (includeSystemTZ)
import Data.Time.Clock.POSIX
import Language.Haskell.TH import Language.Haskell.TH
import Language.Haskell.TH.Syntax (Lift(..)) import Language.Haskell.TH.Syntax (Lift(..))
@ -35,11 +33,8 @@ import Data.Aeson.TH
import Utils.PathPiece import Utils.PathPiece
deriving instance Lift TimeZone import Data.Time.Format.Instances ()
deriving instance Lift TimeLocale
instance Hashable UTCTime where
hashWithSalt s = hashWithSalt s . toRational . utcTimeToPOSIXSeconds
-- $(timeLocaleMap _) :: [Lang] -> TimeLocale -- $(timeLocaleMap _) :: [Lang] -> TimeLocale
timeLocaleMap :: [(Lang, String)] -- ^ Languages and matching locales, first is taken as default timeLocaleMap :: [(Lang, String)] -- ^ Languages and matching locales, first is taken as default
@ -91,7 +86,7 @@ instance Finite SelDateTimeFormat
instance Hashable SelDateTimeFormat instance Hashable SelDateTimeFormat
deriveJSON defaultOptions deriveJSON defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 2 . splitCamel { constructorTagModifier = camelToPathPiece' 2
} ''SelDateTimeFormat } ''SelDateTimeFormat
instance ToJSONKey SelDateTimeFormat where instance ToJSONKey SelDateTimeFormat where

View File

@ -5,6 +5,7 @@ module Utils.PathPiece
, splitCamel , splitCamel
, camelToPathPiece, camelToPathPiece' , camelToPathPiece, camelToPathPiece'
, tuplePathPiece , tuplePathPiece
, pathPieceJSONKey
) where ) where
import ClassyPrelude.Yesod import ClassyPrelude.Yesod
@ -22,6 +23,8 @@ import qualified Data.Map as Map
import Numeric.Natural import Numeric.Natural
import Data.List (foldl) import Data.List (foldl)
import Data.Aeson.Types
finiteFromPathPiece :: (PathPiece a, Finite a) => Text -> Maybe a finiteFromPathPiece :: (PathPiece a, Finite a) => Text -> Maybe a
@ -109,3 +112,13 @@ tuplePathPiece tupleDim = do
]) [] ]) []
] ]
] ]
pathPieceJSONKey :: Name -> DecsQ
-- ^ Derive `ToJSONKey`- and `FromJSONKey`-Instances from a `PathPiece`-Instance
pathPieceJSONKey tName
= [d| instance ToJSONKey $(conT tName) where
toJSONKey = toJSONKeyText toPathPiece
instance FromJSONKey $(conT tName) where
fromJSONKey = FromJSONKeyTextParser $ \t -> maybe (fail $ "Could not parse " <> unpack t <> " as value for " <> $(TH.lift $ nameBase tName) <> "via PathPiece") return $ fromPathPiece t
|]

View File

@ -27,7 +27,7 @@ spec = do
lawsCheckHspec (Proxy @MailSmtpData) lawsCheckHspec (Proxy @MailSmtpData)
[ eqLaws, ordLaws, showReadLaws, monoidLaws ] [ eqLaws, ordLaws, showReadLaws, monoidLaws ]
lawsCheckHspec (Proxy @MailLanguages) lawsCheckHspec (Proxy @MailLanguages)
[ eqLaws, ordLaws, showReadLaws, isListLaws, jsonLaws, hashableLaws ] [ eqLaws, ordLaws, showReadLaws, isListLaws, jsonLaws, hashableLaws, persistFieldLaws ]
lawsCheckHspec (Proxy @MailContext) lawsCheckHspec (Proxy @MailContext)
[ eqLaws, ordLaws, showReadLaws, jsonLaws, hashableLaws ] [ eqLaws, ordLaws, showReadLaws, jsonLaws, hashableLaws ]
lawsCheckHspec (Proxy @VerpMode) lawsCheckHspec (Proxy @VerpMode)

View File

@ -267,8 +267,6 @@ spec = do
[ eqLaws, ordLaws, showReadLaws, jsonLaws, boundedEnumLaws, finiteLaws, hashableLaws, jsonLaws, jsonKeyLaws ] [ eqLaws, ordLaws, showReadLaws, jsonLaws, boundedEnumLaws, finiteLaws, hashableLaws, jsonLaws, jsonKeyLaws ]
lawsCheckHspec (Proxy @NotificationSettings) lawsCheckHspec (Proxy @NotificationSettings)
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ] [ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
lawsCheckHspec (Proxy @MailLanguages)
[ persistFieldLaws ]
lawsCheckHspec (Proxy @Pseudonym) lawsCheckHspec (Proxy @Pseudonym)
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, integralLaws, jsonLaws, persistFieldLaws ] [ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, integralLaws, jsonLaws, persistFieldLaws ]
lawsCheckHspec (Proxy @AuthTag) lawsCheckHspec (Proxy @AuthTag)

View File

@ -32,6 +32,7 @@ import Data.Proxy as X
import Data.UUID as X (UUID) import Data.UUID as X (UUID)
import System.IO as X (hPrint, hPutStrLn, stderr) import System.IO as X (hPrint, hPutStrLn, stderr)
import Jobs (handleJobs, stopJobCtl) import Jobs (handleJobs, stopJobCtl)
import Numeric.Natural as X
import Control.Lens as X hiding ((<.), elements) import Control.Lens as X hiding ((<.), elements)

View File

@ -2,6 +2,9 @@ module Utils.DateTimeSpec where
import TestImport import TestImport
import Utils.DateTime
instance Arbitrary DateTimeFormat where instance Arbitrary DateTimeFormat where
arbitrary = DateTimeFormat <$> arbitrary arbitrary = DateTimeFormat <$> arbitrary
shrink = genericShrink shrink = genericShrink