feat(caching): aggressively cache nav items
This commit is contained in:
parent
466203d866
commit
b9b090992f
@ -196,3 +196,9 @@ allocation-grade-ordinal-proportion: 0.075
|
|||||||
|
|
||||||
instance-id: "_env:INSTANCE_ID:instance"
|
instance-id: "_env:INSTANCE_ID:instance"
|
||||||
ribbon: "_env:RIBBON:"
|
ribbon: "_env:RIBBON:"
|
||||||
|
|
||||||
|
|
||||||
|
favourites-quick-actions-burstsize: 40
|
||||||
|
favourites-quick-actions-avg-inverse-rate: 50e3 # µs/token
|
||||||
|
favourites-quick-actions-timeout: 40e-3 # s
|
||||||
|
favourites-quick-actions-cache-ttl: 120 # s
|
||||||
|
|||||||
@ -232,6 +232,13 @@
|
|||||||
.asidenav__nested-list
|
.asidenav__nested-list
|
||||||
min-width: 200px
|
min-width: 200px
|
||||||
|
|
||||||
|
.asidenav__nested-list--unavailable
|
||||||
|
font-size: 0.9rem
|
||||||
|
color: var(--color-fontsec)
|
||||||
|
font-weight: 600
|
||||||
|
padding: 7px
|
||||||
|
min-width: 200px
|
||||||
|
|
||||||
@media (max-width: 425px)
|
@media (max-width: 425px)
|
||||||
.asidenav__list-item
|
.asidenav__list-item
|
||||||
padding-left: 10px
|
padding-left: 10px
|
||||||
@ -239,6 +246,9 @@
|
|||||||
.asidenav__nested-list
|
.asidenav__nested-list
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
|
.asidenav__nested-list--unavailable
|
||||||
|
display: none
|
||||||
|
|
||||||
.asidenav__nested-list-item
|
.asidenav__nested-list-item
|
||||||
position: relative
|
position: relative
|
||||||
|
|
||||||
@ -317,8 +327,7 @@
|
|||||||
color: var(--color-font)
|
color: var(--color-font)
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
.asidenav__nested-list,
|
.asidenav__nested-list, .asidenav__link-label, .asidenav__nested-list--unavailable
|
||||||
.asidenav__link-label
|
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
.asidenav__list-item--active
|
.asidenav__list-item--active
|
||||||
|
|||||||
@ -2247,6 +2247,8 @@ FavouriteParticipant: Ihre Kurse
|
|||||||
FavouriteManual: Favoriten
|
FavouriteManual: Favoriten
|
||||||
FavouriteCurrent: Aktueller Kurs
|
FavouriteCurrent: Aktueller Kurs
|
||||||
|
|
||||||
|
FavouritesUnavailableTip: Das Schnellzugriffsmenü für diesen Kurs ist aktuell nicht verfügbar.
|
||||||
|
|
||||||
CourseEvents: Termine
|
CourseEvents: Termine
|
||||||
CourseEventType: Art
|
CourseEventType: Art
|
||||||
CourseEventTypePlaceholder: Vorlesung, Zentralübung, ...
|
CourseEventTypePlaceholder: Vorlesung, Zentralübung, ...
|
||||||
|
|||||||
@ -2247,6 +2247,8 @@ FavouriteParticipant: Your courses
|
|||||||
FavouriteManual: Favourites
|
FavouriteManual: Favourites
|
||||||
FavouriteCurrent: Current course
|
FavouriteCurrent: Current course
|
||||||
|
|
||||||
|
FavouritesUnavailableTip: Quick Actions for this course are currently not available.
|
||||||
|
|
||||||
CourseEvents: Occurrences
|
CourseEvents: Occurrences
|
||||||
CourseEventType: Type
|
CourseEventType: Type
|
||||||
CourseEventTypePlaceholder: Lecture, Exercise discussion, ...
|
CourseEventTypePlaceholder: Lecture, Exercise discussion, ...
|
||||||
|
|||||||
@ -9,6 +9,7 @@ module Foundation
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import Foundation.Type as Foundation
|
import Foundation.Type as Foundation
|
||||||
|
import Foundation.Types as Foundation
|
||||||
import Foundation.I18n as Foundation
|
import Foundation.I18n as Foundation
|
||||||
import Foundation.Routes as Foundation
|
import Foundation.Routes as Foundation
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ import Handler.Utils.ExamOffice.ExternalExam
|
|||||||
import Handler.Utils.ExamOffice.Course
|
import Handler.Utils.ExamOffice.Course
|
||||||
import Handler.Utils.Profile
|
import Handler.Utils.Profile
|
||||||
import Handler.Utils.Routes
|
import Handler.Utils.Routes
|
||||||
|
import Handler.Utils.Memcached
|
||||||
import Utils.Form
|
import Utils.Form
|
||||||
import Utils.Sheet
|
import Utils.Sheet
|
||||||
import Utils.SystemMessage
|
import Utils.SystemMessage
|
||||||
@ -121,8 +123,7 @@ data NavQuickView
|
|||||||
= NavQuickViewFavourite
|
= NavQuickViewFavourite
|
||||||
| NavQuickViewPageActionSecondary
|
| NavQuickViewPageActionSecondary
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
instance Universe NavQuickView
|
deriving (Universe, Finite)
|
||||||
instance Finite NavQuickView
|
|
||||||
|
|
||||||
navQuick :: NavQuickView -> (NavQuickView -> Any)
|
navQuick :: NavQuickView -> (NavQuickView -> Any)
|
||||||
navQuick x x' = Any $ x == x'
|
navQuick x x' = Any $ x == x'
|
||||||
@ -134,7 +135,9 @@ data NavType
|
|||||||
| NavTypeButton
|
| NavTypeButton
|
||||||
{ navMethod :: StdMethod
|
{ navMethod :: StdMethod
|
||||||
, navData :: [(Text, Text)]
|
, navData :: [(Text, Text)]
|
||||||
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
}
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
deriving anyclass (Binary)
|
||||||
|
|
||||||
makeLenses_ ''NavType
|
makeLenses_ ''NavType
|
||||||
makePrisms ''NavType
|
makePrisms ''NavType
|
||||||
@ -211,7 +214,10 @@ navLinkAccess NavLink{..} = handle shortCircuit $ liftHandler navAccess' `and2M`
|
|||||||
shortCircuit _ = return False
|
shortCircuit _ = return False
|
||||||
|
|
||||||
accessCheck :: HasRoute UniWorX route => NavType -> route -> m Bool
|
accessCheck :: HasRoute UniWorX route => NavType -> route -> m Bool
|
||||||
accessCheck nt (urlRoute -> route) = bool hasWriteAccessTo hasReadAccessTo (is _NavTypeLink nt) route
|
accessCheck nt (urlRoute -> route) = do
|
||||||
|
authCtx <- getAuthContext
|
||||||
|
$memcachedByHere (Just $ Right 120) (authCtx, nt, route) $
|
||||||
|
bool hasWriteAccessTo hasReadAccessTo (is _NavTypeLink nt) route
|
||||||
|
|
||||||
|
|
||||||
getTimeLocale' :: [Lang] -> TimeLocale
|
getTimeLocale' :: [Lang] -> TimeLocale
|
||||||
@ -304,12 +310,31 @@ trueAP = APPure . const . const . const $ trueAR <$> ask
|
|||||||
falseAP = APPure . const . const . const $ falseAR <$> ask -- included for completeness
|
falseAP = APPure . const . const . const $ falseAR <$> ask -- included for completeness
|
||||||
|
|
||||||
|
|
||||||
|
data AuthContext = AuthContext
|
||||||
|
{ authCtxAuth :: Maybe UserId
|
||||||
|
, authCtxBearer :: Maybe (BearerToken UniWorX)
|
||||||
|
, authActiveTags :: Set AuthTag
|
||||||
|
} deriving (Eq, Read, Show, Generic, Typeable)
|
||||||
|
deriving anyclass (Hashable, Binary)
|
||||||
|
|
||||||
|
getAuthContext :: forall m.
|
||||||
|
( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, MonadCatch m
|
||||||
|
)
|
||||||
|
=> m AuthContext
|
||||||
|
getAuthContext = AuthContext
|
||||||
|
<$> maybeAuthId
|
||||||
|
<*> runMaybeT (exceptTMaybe askBearerUnsafe)
|
||||||
|
<*> (fromMaybe def <$> lookupSessionJson SessionActiveAuthTags)
|
||||||
|
|
||||||
|
|
||||||
askBearerUnsafe :: forall m.
|
askBearerUnsafe :: forall m.
|
||||||
( MonadHandler m
|
( MonadHandler m
|
||||||
, HandlerSite m ~ UniWorX
|
, HandlerSite m ~ UniWorX
|
||||||
, MonadCatch m
|
, MonadCatch m
|
||||||
)
|
)
|
||||||
=> ExceptT AuthResult m (BearerToken (UniWorX))
|
=> ExceptT AuthResult m (BearerToken UniWorX)
|
||||||
-- | This performs /no/ meaningful validation of the `BearerToken`
|
-- | This performs /no/ meaningful validation of the `BearerToken`
|
||||||
--
|
--
|
||||||
-- Use `Handler.Utils.Tokens.requireBearerToken` or `Handler.Utils.Tokens.maybeBearerToken` instead
|
-- Use `Handler.Utils.Tokens.requireBearerToken` or `Handler.Utils.Tokens.maybeBearerToken` instead
|
||||||
@ -1462,47 +1487,17 @@ data instance ButtonClass UniWorX
|
|||||||
| BCLink
|
| BCLink
|
||||||
| BCMassInputAdd | BCMassInputDelete
|
| BCMassInputAdd | BCMassInputDelete
|
||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
instance Universe (ButtonClass UniWorX)
|
deriving anyclass (Universe, Finite)
|
||||||
instance Finite (ButtonClass UniWorX)
|
|
||||||
|
|
||||||
instance PathPiece (ButtonClass UniWorX) where
|
instance PathPiece (ButtonClass UniWorX) where
|
||||||
toPathPiece BCIsButton = "btn"
|
toPathPiece BCIsButton = "btn"
|
||||||
toPathPiece bClass = ("btn-" <>) . camelToPathPiece' 1 $ tshow bClass
|
toPathPiece bClass = ("btn-" <>) . camelToPathPiece' 1 $ tshow bClass
|
||||||
fromPathPiece = flip List.lookup $ map (toPathPiece &&& id) universeF
|
fromPathPiece = flip List.lookup $ map (toPathPiece &&& id) universeF
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''ButtonSubmit id
|
|
||||||
instance Button UniWorX ButtonSubmit where
|
instance Button UniWorX ButtonSubmit where
|
||||||
btnClasses BtnSubmit = [BCIsButton, BCPrimary]
|
btnClasses BtnSubmit = [BCIsButton, BCPrimary]
|
||||||
|
|
||||||
|
|
||||||
updateFavourites :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX)
|
|
||||||
=> Maybe (TermId, SchoolId, CourseShorthand) -- ^ Insert course into favourites, as appropriate
|
|
||||||
-> ReaderT SqlBackend m ()
|
|
||||||
updateFavourites cData = void . runMaybeT $ do
|
|
||||||
$logDebugS "updateFavourites" "Updating favourites"
|
|
||||||
|
|
||||||
now <- liftIO $ getCurrentTime
|
|
||||||
uid <- MaybeT $ liftHandler maybeAuthId
|
|
||||||
mcid <- for cData $ \(tid, ssh, csh) -> MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
|
||||||
User{userMaxFavourites} <- MaybeT $ get uid
|
|
||||||
|
|
||||||
-- update Favourites
|
|
||||||
for_ mcid $ \cid ->
|
|
||||||
void . lift $ upsertBy
|
|
||||||
(UniqueCourseFavourite uid cid)
|
|
||||||
(CourseFavourite uid cid FavouriteVisited now)
|
|
||||||
[CourseFavouriteLastVisit =. now]
|
|
||||||
-- prune Favourites to user-defined size
|
|
||||||
oldFavs <- lift $ selectList [CourseFavouriteUser ==. uid] []
|
|
||||||
let deleteFavs = oldFavs
|
|
||||||
& sortOn ((courseFavouriteReason &&& Down . courseFavouriteLastVisit) . entityVal)
|
|
||||||
& drop userMaxFavourites
|
|
||||||
& filter ((<= FavouriteVisited) . courseFavouriteReason . entityVal)
|
|
||||||
& map entityKey
|
|
||||||
unless (null deleteFavs) $
|
|
||||||
lift $ deleteWhere [CourseFavouriteId <-. deleteFavs]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Please see the documentation for the Yesod typeclass. There are a number
|
-- Please see the documentation for the Yesod typeclass. There are a number
|
||||||
-- of settings which can be configured by overriding methods here.
|
-- of settings which can be configured by overriding methods here.
|
||||||
@ -1717,6 +1712,45 @@ instance Yesod UniWorX where
|
|||||||
-- (Just lang)
|
-- (Just lang)
|
||||||
-- return ((,) <$> langBoxRes <*> urlRes, toWidget csrf <> fvInput urlView <> fvInput langBoxView)
|
-- return ((,) <$> langBoxRes <*> urlRes, toWidget csrf <> fvInput urlView <> fvInput langBoxView)
|
||||||
|
|
||||||
|
data MemcachedKeyFavourites
|
||||||
|
= MemcachedKeyFavouriteQuickActions CourseId AuthContext
|
||||||
|
deriving (Eq, Read, Show, Generic, Typeable)
|
||||||
|
deriving anyclass (Hashable, Binary)
|
||||||
|
|
||||||
|
data MemcachedLimitKeyFavourites
|
||||||
|
= MemcachedLimitKeyFavourites
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
deriving anyclass (Hashable, Binary)
|
||||||
|
|
||||||
|
|
||||||
|
updateFavourites :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX)
|
||||||
|
=> Maybe (TermId, SchoolId, CourseShorthand) -- ^ Insert course into favourites, as appropriate
|
||||||
|
-> ReaderT SqlBackend m ()
|
||||||
|
updateFavourites cData = void . runMaybeT $ do
|
||||||
|
$logDebugS "updateFavourites" "Updating favourites"
|
||||||
|
|
||||||
|
now <- liftIO $ getCurrentTime
|
||||||
|
uid <- MaybeT $ liftHandler maybeAuthId
|
||||||
|
mcid <- for cData $ \(tid, ssh, csh) -> MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
||||||
|
User{userMaxFavourites} <- MaybeT $ get uid
|
||||||
|
|
||||||
|
-- update Favourites
|
||||||
|
for_ mcid $ \cid ->
|
||||||
|
void . lift $ upsertBy
|
||||||
|
(UniqueCourseFavourite uid cid)
|
||||||
|
(CourseFavourite uid cid FavouriteVisited now)
|
||||||
|
[CourseFavouriteLastVisit =. now]
|
||||||
|
-- prune Favourites to user-defined size
|
||||||
|
oldFavs <- lift $ selectList [CourseFavouriteUser ==. uid] []
|
||||||
|
let deleteFavs = oldFavs
|
||||||
|
& sortOn ((courseFavouriteReason &&& Down . courseFavouriteLastVisit) . entityVal)
|
||||||
|
& drop userMaxFavourites
|
||||||
|
& filter ((<= FavouriteVisited) . courseFavouriteReason . entityVal)
|
||||||
|
& map entityKey
|
||||||
|
unless (null deleteFavs) $
|
||||||
|
lift $ deleteWhere [CourseFavouriteId <-. deleteFavs]
|
||||||
|
|
||||||
|
|
||||||
siteLayoutMsg :: (RenderMessage site msg, site ~ UniWorX) => msg -> Widget -> Handler Html
|
siteLayoutMsg :: (RenderMessage site msg, site ~ UniWorX) => msg -> Widget -> Handler Html
|
||||||
siteLayoutMsg msg widget = do
|
siteLayoutMsg msg widget = do
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
@ -1812,13 +1846,32 @@ siteLayout' headingOverride widget = do
|
|||||||
, maybe userDefaultMaxFavouriteTerms userMaxFavouriteTerms $ view _2 <$> muid
|
, maybe userDefaultMaxFavouriteTerms userMaxFavouriteTerms $ view _2 <$> muid
|
||||||
, maybe userDefaultTheme userTheme $ view _2 <$> muid
|
, maybe userDefaultTheme userTheme $ view _2 <$> muid
|
||||||
)
|
)
|
||||||
favourites <- forM favourites' $ \(Entity _ c@Course{..}, E.Value mFavourite)
|
|
||||||
|
let favouriteTerms :: [TermIdentifier]
|
||||||
|
favouriteTerms = take maxFavouriteTerms . Set.toDescList $ foldMap (\(Entity _ Course{..}, _) -> Set.singleton $ unTermKey courseTerm) favourites'
|
||||||
|
|
||||||
|
favourites <- fmap catMaybes . forM favourites' $ \(Entity cId c@Course{..}, E.Value mFavourite)
|
||||||
-> let courseRoute = CourseR courseTerm courseSchool courseShorthand CShowR
|
-> let courseRoute = CourseR courseTerm courseSchool courseShorthand CShowR
|
||||||
favouriteReason = fromMaybe FavouriteCurrent mFavourite
|
favouriteReason = fromMaybe FavouriteCurrent mFavourite
|
||||||
in do
|
in runMaybeT . guardOnM (unTermKey courseTerm `elem` favouriteTerms) . lift $ do
|
||||||
items' <- pageQuickActions NavQuickViewFavourite courseRoute
|
ctx <- getAuthContext
|
||||||
items <- forM items' $ \n -> (n,) <$> toTextUrl n
|
MsgRenderer mr <- getMsgRenderer
|
||||||
return (c, courseRoute, items, favouriteReason)
|
let cK = MemcachedKeyFavouriteQuickActions cId ctx
|
||||||
|
$logDebugS "FavouriteQuickActions" $ tshow cK <> " Checking..."
|
||||||
|
items <- memcachedLimitedKeyTimeoutBy
|
||||||
|
MemcachedLimitKeyFavourites appFavouritesQuickActionsBurstsize appFavouritesQuickActionsAvgInverseRate 1
|
||||||
|
(Right <$> appFavouritesQuickActionsCacheTTL)
|
||||||
|
appFavouritesQuickActionsTimeout
|
||||||
|
cK
|
||||||
|
cK
|
||||||
|
. observeFavouritesQuickActionsDuration $ do
|
||||||
|
$logDebugS "FavouriteQuickActions" $ tshow cK <> " Starting..."
|
||||||
|
items' <- pageQuickActions NavQuickViewFavourite courseRoute
|
||||||
|
items <- forM items' $ \n@NavLink{navLabel} -> (mr navLabel,) <$> toTextUrl n
|
||||||
|
$logDebugS "FavouriteQuickActions" $ tshow cK <> " Done."
|
||||||
|
return items
|
||||||
|
$logDebugS "FavouriteQuickActions" $ tshow cK <> " returning " <> tshow (is _Just items)
|
||||||
|
return (c, courseRoute, items, favouriteReason)
|
||||||
|
|
||||||
nav'' <- mconcat <$> sequence
|
nav'' <- mconcat <$> sequence
|
||||||
[ defaultLinks
|
[ defaultLinks
|
||||||
@ -1850,9 +1903,7 @@ siteLayout' headingOverride widget = do
|
|||||||
navItems = map (view _2) favourites ++ toListOf (folded . typesUsing @NavChildren @NavLink . to urlRoute) nav
|
navItems = map (view _2) favourites ++ toListOf (folded . typesUsing @NavChildren @NavLink . to urlRoute) nav
|
||||||
highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map (view _2) favourites) crumbs
|
highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map (view _2) favourites) crumbs
|
||||||
highlightNav = (||) <$> navForceActive <*> highlight
|
highlightNav = (||) <$> navForceActive <*> highlight
|
||||||
favouriteTerms :: [TermIdentifier]
|
favouriteTermReason :: TermIdentifier -> FavouriteReason -> [(Course, Route UniWorX, Maybe [(Text, Text)], FavouriteReason)]
|
||||||
favouriteTerms = take maxFavouriteTerms . Set.toDescList $ foldMap (\(Course{..}, _, _, _) -> Set.singleton $ unTermKey courseTerm) favourites
|
|
||||||
favouriteTermReason :: TermIdentifier -> FavouriteReason -> [(Course, Route UniWorX, [(NavLink, Text)], FavouriteReason)]
|
|
||||||
favouriteTermReason tid favReason' = favourites
|
favouriteTermReason tid favReason' = favourites
|
||||||
& filter (\(Course{..}, _, _, favReason) -> unTermKey courseTerm == tid && favReason == favReason')
|
& filter (\(Course{..}, _, _, favReason) -> unTermKey courseTerm == tid && favReason == favReason')
|
||||||
& sortOn (\(Course{..}, _, _, _) -> courseName)
|
& sortOn (\(Course{..}, _, _, _) -> courseName)
|
||||||
@ -4325,17 +4376,7 @@ data CampusUserConversionException
|
|||||||
| CampusUserInvalidFeaturesOfStudy Text
|
| CampusUserInvalidFeaturesOfStudy Text
|
||||||
| CampusUserInvalidAssociatedSchools Text
|
| CampusUserInvalidAssociatedSchools Text
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
instance Exception CampusUserConversionException
|
deriving anyclass (Exception)
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''CampusUserConversionException id
|
|
||||||
|
|
||||||
data UpsertCampusUserMode
|
|
||||||
= UpsertCampusUser
|
|
||||||
| UpsertCampusUserDummy { upsertCampusUserIdent :: UserIdent }
|
|
||||||
| UpsertCampusUserOther { uspertCampusUserIdent :: UserIdent }
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
|
||||||
makeLenses_ ''UpsertCampusUserMode
|
|
||||||
makePrisms ''UpsertCampusUserMode
|
|
||||||
|
|
||||||
_upsertCampusUserMode :: Traversal' (Creds UniWorX) UpsertCampusUserMode
|
_upsertCampusUserMode :: Traversal' (Creds UniWorX) UpsertCampusUserMode
|
||||||
_upsertCampusUserMode mMode cs@Creds{..}
|
_upsertCampusUserMode mMode cs@Creds{..}
|
||||||
@ -4812,6 +4853,7 @@ instance YesodAuth UniWorX where
|
|||||||
|
|
||||||
instance YesodAuthPersist UniWorX
|
instance YesodAuthPersist UniWorX
|
||||||
|
|
||||||
|
|
||||||
unsafeHandler :: UniWorX -> Handler a -> IO a
|
unsafeHandler :: UniWorX -> Handler a -> IO a
|
||||||
unsafeHandler f h = do
|
unsafeHandler f h = do
|
||||||
logger <- makeLogger f
|
logger <- makeLogger f
|
||||||
@ -4852,3 +4894,8 @@ instance {-# OVERLAPPING #-} (Monad m, MonadHandler m, HandlerSite m ~ UniWorX)
|
|||||||
-- https://github.com/yesodweb/yesod/wiki/Sending-email
|
-- https://github.com/yesodweb/yesod/wiki/Sending-email
|
||||||
-- https://github.com/yesodweb/yesod/wiki/Serve-static-files-from-a-separate-domain
|
-- https://github.com/yesodweb/yesod/wiki/Serve-static-files-from-a-separate-domain
|
||||||
-- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding
|
-- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding
|
||||||
|
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''ButtonSubmit id
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''CampusUserConversionException id
|
||||||
|
|||||||
17
src/Foundation/Types.hs
Normal file
17
src/Foundation/Types.hs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module Foundation.Types
|
||||||
|
( UpsertCampusUserMode(..)
|
||||||
|
, _UpsertCampusUser, _UpsertCampusUserDummy, _UpsertCampusUserOther
|
||||||
|
, _upsertCampusUserIdent
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import.NoFoundation
|
||||||
|
|
||||||
|
|
||||||
|
data UpsertCampusUserMode
|
||||||
|
= UpsertCampusUser
|
||||||
|
| UpsertCampusUserDummy { upsertCampusUserIdent :: UserIdent }
|
||||||
|
| UpsertCampusUserOther { uspertCampusUserIdent :: UserIdent }
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
makeLenses_ ''UpsertCampusUserMode
|
||||||
|
makePrisms ''UpsertCampusUserMode
|
||||||
@ -2,6 +2,7 @@ module Handler.Utils.Memcached
|
|||||||
( memcached, memcachedBy
|
( memcached, memcachedBy
|
||||||
, memcachedHere, memcachedByHere
|
, memcachedHere, memcachedByHere
|
||||||
, memcachedSet, memcachedGet
|
, memcachedSet, memcachedGet
|
||||||
|
, memcachedInvalidate, memcachedByInvalidate
|
||||||
, memcachedByGet, memcachedBySet
|
, memcachedByGet, memcachedBySet
|
||||||
, memcachedTimeout, memcachedTimeoutBy
|
, memcachedTimeout, memcachedTimeoutBy
|
||||||
, memcachedTimeoutHere, memcachedTimeoutByHere
|
, memcachedTimeoutHere, memcachedTimeoutByHere
|
||||||
@ -13,7 +14,8 @@ module Handler.Utils.Memcached
|
|||||||
, MemcachedException(..), AsyncTimeoutException(..)
|
, MemcachedException(..), AsyncTimeoutException(..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import hiding (utc, exp)
|
import Import.NoFoundation hiding (utc, exp)
|
||||||
|
import Foundation.Type
|
||||||
|
|
||||||
|
|
||||||
import qualified Database.Memcached.Binary.IO as Memcached
|
import qualified Database.Memcached.Binary.IO as Memcached
|
||||||
@ -98,16 +100,22 @@ memcachedByGet k = runMaybeT $ do
|
|||||||
(aeadKey, conn) <- MaybeT $ getsYesod appMemcached
|
(aeadKey, conn) <- MaybeT $ getsYesod appMemcached
|
||||||
let cKey = memcachedKey aeadKey (Proxy @a) k
|
let cKey = memcachedKey aeadKey (Proxy @a) k
|
||||||
|
|
||||||
encVal <- fmap toStrict . hoist liftIO . catchIfMaybeT Memcached.isItemNotStored $ Memcached.get_ cKey conn
|
encVal <- fmap toStrict . hoist liftIO . catchMaybeT (Proxy @Memcached.MemcachedException) $ Memcached.get_ cKey conn
|
||||||
|
|
||||||
|
$logInfoS "memcached" "Cache hit"
|
||||||
|
|
||||||
guard $ length encVal >= Saltine.secretBoxNonce + Saltine.secretBoxMac
|
guard $ length encVal >= Saltine.secretBoxNonce + Saltine.secretBoxMac
|
||||||
let (nonceBS, encrypted) = splitAt Saltine.secretBoxNonce encVal
|
let (nonceBS, encrypted) = splitAt Saltine.secretBoxNonce encVal
|
||||||
nonce <- hoistMaybe $ Saltine.decode nonceBS
|
nonce <- hoistMaybe $ Saltine.decode nonceBS
|
||||||
decrypted <- hoistMaybe $ AEAD.aeadOpen aeadKey nonce encrypted cKey
|
decrypted <- hoistMaybe $ AEAD.aeadOpen aeadKey nonce encrypted cKey
|
||||||
|
|
||||||
|
$logDebugS "memcached" "Decryption valid"
|
||||||
|
|
||||||
case Binary.decodeOrFail $ fromStrict decrypted of
|
case Binary.decodeOrFail $ fromStrict decrypted of
|
||||||
Right (unconsumed, _, v)
|
Right (unconsumed, _, v)
|
||||||
| null unconsumed -> return v
|
| null unconsumed -> do
|
||||||
|
$logDebugS "memcached" "Deserialization valid"
|
||||||
|
return v
|
||||||
_other -> mzero
|
_other -> mzero
|
||||||
|
|
||||||
memcachedBySet :: forall a k m.
|
memcachedBySet :: forall a k m.
|
||||||
@ -124,7 +132,19 @@ memcachedBySet mExp k v = do
|
|||||||
nonce <- liftIO AEAD.newNonce
|
nonce <- liftIO AEAD.newNonce
|
||||||
let cKey = memcachedKey aeadKey (Proxy @a) k
|
let cKey = memcachedKey aeadKey (Proxy @a) k
|
||||||
encVal = Saltine.encode nonce <> AEAD.aead aeadKey nonce (toStrict $ Binary.encode v) cKey
|
encVal = Saltine.encode nonce <> AEAD.aead aeadKey nonce (toStrict $ Binary.encode v) cKey
|
||||||
liftIO $ Memcached.add zeroBits (fromMaybe zeroBits mExp') cKey (fromStrict encVal) conn
|
liftIO $ Memcached.set zeroBits (fromMaybe zeroBits mExp') cKey (fromStrict encVal) conn
|
||||||
|
$logInfoS "memcached" "Cache store"
|
||||||
|
|
||||||
|
memcachedByInvalidate :: forall a k m p.
|
||||||
|
( MonadHandler m, HandlerSite m ~ UniWorX
|
||||||
|
, Typeable a
|
||||||
|
, Binary k
|
||||||
|
)
|
||||||
|
=> k -> p a -> m ()
|
||||||
|
memcachedByInvalidate k _ = maybeT_ $ do
|
||||||
|
(aeadKey, conn) <- MaybeT $ getsYesod appMemcached
|
||||||
|
let cKey = memcachedKey aeadKey (Proxy @a) k
|
||||||
|
hoist liftIO . catchIfMaybeT Memcached.isKeyNotFound $ Memcached.delete cKey conn
|
||||||
|
|
||||||
|
|
||||||
newtype MemcachedUnkeyed a = MemcachedUnkeyed { unMemcachedUnkeyed :: a }
|
newtype MemcachedUnkeyed a = MemcachedUnkeyed { unMemcachedUnkeyed :: a }
|
||||||
@ -144,6 +164,13 @@ memcachedSet :: ( MonadHandler m, HandlerSite m ~ UniWorX
|
|||||||
=> Maybe Expiry -> a -> m ()
|
=> Maybe Expiry -> a -> m ()
|
||||||
memcachedSet mExp = memcachedBySet mExp () . MemcachedUnkeyed
|
memcachedSet mExp = memcachedBySet mExp () . MemcachedUnkeyed
|
||||||
|
|
||||||
|
memcachedInvalidate :: forall (a :: *) m p.
|
||||||
|
( MonadHandler m, HandlerSite m ~ UniWorX
|
||||||
|
, Typeable a
|
||||||
|
)
|
||||||
|
=> p a -> m ()
|
||||||
|
memcachedInvalidate _ = memcachedByInvalidate () $ Proxy @(MemcachedUnkeyed a)
|
||||||
|
|
||||||
|
|
||||||
memcachedWith :: Monad m
|
memcachedWith :: Monad m
|
||||||
=> (m (Maybe a), a -> m ()) -> m a -> m a
|
=> (m (Maybe a), a -> m ()) -> m a -> m a
|
||||||
@ -208,6 +235,7 @@ memcachedLimit = unsafePerformIO . newTVarIO $ HashMap.empty
|
|||||||
{-# NOINLINE memcachedLimit #-}
|
{-# NOINLINE memcachedLimit #-}
|
||||||
|
|
||||||
memcachedLimitedWith :: ( MonadIO m
|
memcachedLimitedWith :: ( MonadIO m
|
||||||
|
, MonadLogger m
|
||||||
, Typeable k', Hashable k', Eq k'
|
, Typeable k', Hashable k', Eq k'
|
||||||
)
|
)
|
||||||
=> (m (Maybe a), a -> m ())
|
=> (m (Maybe a), a -> m ())
|
||||||
@ -231,7 +259,9 @@ memcachedLimitedWith (doGet, doSet) liftAct (hashableDynamic -> lK) burst rate t
|
|||||||
let hm' = HashMap.insertWith (flip const) lK bucket hm
|
let hm' = HashMap.insertWith (flip const) lK bucket hm
|
||||||
writeTVar memcachedLimit $! hm'
|
writeTVar memcachedLimit $! hm'
|
||||||
return $ HashMap.lookupDefault (error "could not insert new token bucket") lK hm'
|
return $ HashMap.lookupDefault (error "could not insert new token bucket") lK hm'
|
||||||
guardM . liftIO $ tokenBucketTryAlloc bucket burst rate tokens
|
sufficientTokens <- liftIO $ tokenBucketTryAlloc bucket burst rate tokens
|
||||||
|
$logDebugS "memcachedLimitedWith" $ "Sufficient tokens: " <> tshow sufficientTokens
|
||||||
|
guard sufficientTokens
|
||||||
|
|
||||||
liftAct $ do
|
liftAct $ do
|
||||||
res <- act
|
res <- act
|
||||||
@ -336,6 +366,7 @@ memcachedAsync = unsafePerformIO . newTVarIO $ HashMap.empty
|
|||||||
|
|
||||||
liftAsyncTimeout :: forall k'' a m.
|
liftAsyncTimeout :: forall k'' a m.
|
||||||
( MonadResource m, MonadUnliftIO m
|
( MonadResource m, MonadUnliftIO m
|
||||||
|
, MonadLogger m
|
||||||
, MonadThrow m
|
, MonadThrow m
|
||||||
, Typeable k'', Hashable k'', Eq k''
|
, Typeable k'', Hashable k'', Eq k''
|
||||||
, Typeable a
|
, Typeable a
|
||||||
@ -344,7 +375,7 @@ liftAsyncTimeout :: forall k'' a m.
|
|||||||
-> k''
|
-> k''
|
||||||
-> m a -> MaybeT m a
|
-> m a -> MaybeT m a
|
||||||
liftAsyncTimeout dt (hashableDynamic -> cK) act = do
|
liftAsyncTimeout dt (hashableDynamic -> cK) act = do
|
||||||
delay <- liftIO . newDelay . round $ toRational dt / 1e6
|
delay <- liftIO . newDelay . round $ toRational dt * 1e6
|
||||||
|
|
||||||
act' <- lift $ do
|
act' <- lift $ do
|
||||||
existing <- traverse castDynamicAsync . HashMap.lookup cK <=< liftIO $ readTVarIO memcachedAsync
|
existing <- traverse castDynamicAsync . HashMap.lookup cK <=< liftIO $ readTVarIO memcachedAsync
|
||||||
@ -352,8 +383,10 @@ liftAsyncTimeout dt (hashableDynamic -> cK) act = do
|
|||||||
Just act' -> return act'
|
Just act' -> return act'
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
startAct <- liftIO newEmptyTMVarIO
|
startAct <- liftIO newEmptyTMVarIO
|
||||||
act' <- allocateLinkedAsync $ do
|
act' <- async $ do
|
||||||
|
$logDebugS "liftAsyncTimeout" $ "Waiting for confirmation..."
|
||||||
atomically $ takeTMVar startAct
|
atomically $ takeTMVar startAct
|
||||||
|
$logDebugS "liftAsyncTimeout" $ "Confirmed."
|
||||||
act
|
act
|
||||||
act'' <- atomically $ do
|
act'' <- atomically $ do
|
||||||
hm <- readTVar memcachedAsync
|
hm <- readTVar memcachedAsync
|
||||||
@ -386,6 +419,7 @@ liftAsyncTimeout dt (hashableDynamic -> cK) act = do
|
|||||||
= throwM AsyncTimeoutReturnTypeDoesNotMatchComputationKey
|
= throwM AsyncTimeoutReturnTypeDoesNotMatchComputationKey
|
||||||
|
|
||||||
memcachedTimeoutWith :: ( MonadResource m, MonadUnliftIO m
|
memcachedTimeoutWith :: ( MonadResource m, MonadUnliftIO m
|
||||||
|
, MonadLogger m
|
||||||
, MonadThrow m
|
, MonadThrow m
|
||||||
, Typeable k'', Hashable k'', Eq k''
|
, Typeable k'', Hashable k'', Eq k''
|
||||||
, Typeable a
|
, Typeable a
|
||||||
|
|||||||
@ -155,6 +155,7 @@ import Prometheus.Instances as Import ()
|
|||||||
import Yesod.Form.Fields.Instances as Import ()
|
import Yesod.Form.Fields.Instances as Import ()
|
||||||
import Data.MonoTraversable.Instances as Import ()
|
import Data.MonoTraversable.Instances as Import ()
|
||||||
import Web.Cookie.Instances as Import ()
|
import Web.Cookie.Instances as Import ()
|
||||||
|
import Network.HTTP.Types.Method.Instances as Import ()
|
||||||
|
|
||||||
import Crypto.Hash as Import (Digest, SHA3_256)
|
import Crypto.Hash as Import (Digest, SHA3_256)
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,8 @@ deriving instance (Eq (AuthId site), Eq (Route site)) => Eq (BearerToken site)
|
|||||||
deriving instance (Read (AuthId site), Eq (Route site), Hashable (Route site), Read (Route site), Hashable (AuthId site), Eq (AuthId site)) => Read (BearerToken site)
|
deriving instance (Read (AuthId site), Eq (Route site), Hashable (Route site), Read (Route site), Hashable (AuthId site), Eq (AuthId site)) => Read (BearerToken site)
|
||||||
deriving instance (Show (AuthId site), Show (Route site), Hashable (AuthId site)) => Show (BearerToken site)
|
deriving instance (Show (AuthId site), Show (Route site), Hashable (AuthId site)) => Show (BearerToken site)
|
||||||
|
|
||||||
|
instance (Hashable (AuthId site), Hashable (Route site)) => Hashable (BearerToken site)
|
||||||
|
|
||||||
instance (Binary (AuthId site), Binary (Route site), Hashable (Route site), Eq (Route site), Hashable (AuthId site), Eq (AuthId site)) => Binary (BearerToken site)
|
instance (Binary (AuthId site), Binary (Route site), Hashable (Route site), Eq (Route site), Hashable (AuthId site), Eq (AuthId site)) => Binary (BearerToken site)
|
||||||
|
|
||||||
makeLenses_ ''BearerToken
|
makeLenses_ ''BearerToken
|
||||||
|
|||||||
@ -18,8 +18,6 @@ import qualified Data.HashMap.Strict as HashMap
|
|||||||
|
|
||||||
import qualified Data.Aeson.Types as Aeson
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
|
||||||
import qualified Data.Binary as Binary
|
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
import Model.Types.TH.PathPiece
|
import Model.Types.TH.PathPiece
|
||||||
@ -86,16 +84,12 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä
|
|||||||
| AuthDevelopment
|
| AuthDevelopment
|
||||||
| AuthFree
|
| AuthFree
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
deriving anyclass (Universe, Finite, Hashable)
|
||||||
instance Universe AuthTag
|
|
||||||
instance Finite AuthTag
|
|
||||||
instance Hashable AuthTag
|
|
||||||
|
|
||||||
nullaryPathPiece ''AuthTag $ camelToPathPiece' 1
|
nullaryPathPiece ''AuthTag $ camelToPathPiece' 1
|
||||||
pathPieceJSON ''AuthTag
|
pathPieceJSON ''AuthTag
|
||||||
pathPieceJSONKey ''AuthTag
|
pathPieceJSONKey ''AuthTag
|
||||||
|
pathPieceBinary ''AuthTag
|
||||||
instance Binary AuthTag
|
|
||||||
|
|
||||||
|
|
||||||
newtype AuthTagActive = AuthTagActive { authTagIsActive :: AuthTag -> Bool }
|
newtype AuthTagActive = AuthTagActive { authTagIsActive :: AuthTag -> Bool }
|
||||||
@ -120,8 +114,8 @@ derivePersistFieldJSON ''AuthTagActive
|
|||||||
|
|
||||||
data PredLiteral a = PLVariable { plVar :: a } | PLNegated { plVar :: a }
|
data PredLiteral a = PLVariable { plVar :: a } | PLNegated { plVar :: a }
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
deriving anyclass (Hashable, Binary)
|
||||||
|
|
||||||
instance Hashable a => Hashable (PredLiteral a)
|
|
||||||
deriveJSON defaultOptions
|
deriveJSON defaultOptions
|
||||||
{ constructorTagModifier = camelToPathPiece' 1
|
{ constructorTagModifier = camelToPathPiece' 1
|
||||||
, sumEncoding = TaggedObject "val" "var"
|
, sumEncoding = TaggedObject "val" "var"
|
||||||
@ -134,12 +128,11 @@ instance PathPiece a => PathPiece (PredLiteral a) where
|
|||||||
fromPathPiece t = PLVariable <$> fromPathPiece t
|
fromPathPiece t = PLVariable <$> fromPathPiece t
|
||||||
<|> PLNegated <$> (Text.stripPrefix "¬" t >>= fromPathPiece)
|
<|> PLNegated <$> (Text.stripPrefix "¬" t >>= fromPathPiece)
|
||||||
|
|
||||||
instance Binary a => Binary (PredLiteral a)
|
|
||||||
|
|
||||||
|
|
||||||
newtype PredDNF a = PredDNF { dnfTerms :: Set (NonNull (Set (PredLiteral a))) }
|
newtype PredDNF a = PredDNF { dnfTerms :: Set (NonNull (Set (PredLiteral a))) }
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
deriving newtype (Semigroup, Monoid)
|
deriving newtype (Semigroup, Monoid)
|
||||||
|
deriving anyclass (Binary, Hashable)
|
||||||
|
|
||||||
$(return [])
|
$(return [])
|
||||||
|
|
||||||
@ -148,10 +141,6 @@ instance ToJSON a => ToJSON (PredDNF a) where
|
|||||||
instance (Ord a, FromJSON a) => FromJSON (PredDNF a) where
|
instance (Ord a, FromJSON a) => FromJSON (PredDNF a) where
|
||||||
parseJSON = $(mkParseJSON predNFAesonOptions ''PredDNF)
|
parseJSON = $(mkParseJSON predNFAesonOptions ''PredDNF)
|
||||||
|
|
||||||
instance (Ord a, Binary a) => Binary (PredDNF a) where
|
|
||||||
get = PredDNF <$> Binary.get
|
|
||||||
put = Binary.put . dnfTerms
|
|
||||||
|
|
||||||
instance (Ord a, PathPiece a) => PathPiece (PredDNF a) where
|
instance (Ord a, PathPiece a) => PathPiece (PredDNF a) where
|
||||||
toPathPiece = Text.unwords . map (Text.intercalate "AND") . map (map toPathPiece . otoList) . otoList . dnfTerms
|
toPathPiece = Text.unwords . map (Text.intercalate "AND") . map (map toPathPiece . otoList) . otoList . dnfTerms
|
||||||
fromPathPiece = fmap (PredDNF . Set.fromList) . mapM (fromNullable <=< foldMapM (fmap Set.singleton . fromPathPiece) . Text.splitOn "AND") . concatMap (Text.splitOn "OR") . Text.words
|
fromPathPiece = fmap (PredDNF . Set.fromList) . mapM (fromNullable <=< foldMapM (fmap Set.singleton . fromPathPiece) . Text.splitOn "AND") . concatMap (Text.splitOn "OR") . Text.words
|
||||||
|
|||||||
14
src/Network/HTTP/Types/Method/Instances.hs
Normal file
14
src/Network/HTTP/Types/Method/Instances.hs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Network.HTTP.Types.Method.Instances
|
||||||
|
(
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude
|
||||||
|
import Data.Binary (Binary)
|
||||||
|
|
||||||
|
import Network.HTTP.Types.Method
|
||||||
|
|
||||||
|
|
||||||
|
deriving instance Generic StdMethod
|
||||||
|
instance Binary StdMethod
|
||||||
@ -160,6 +160,11 @@ data AppSettings = AppSettings
|
|||||||
|
|
||||||
, appMemcachedConf :: Maybe MemcachedConf
|
, appMemcachedConf :: Maybe MemcachedConf
|
||||||
|
|
||||||
|
, appFavouritesQuickActionsBurstsize
|
||||||
|
, appFavouritesQuickActionsAvgInverseRate :: Word64
|
||||||
|
, appFavouritesQuickActionsTimeout :: DiffTime
|
||||||
|
, appFavouritesQuickActionsCacheTTL :: Maybe DiffTime
|
||||||
|
|
||||||
, appInitialInstanceID :: Maybe (Either FilePath UUID)
|
, appInitialInstanceID :: Maybe (Either FilePath UUID)
|
||||||
, appRibbon :: Maybe Text
|
, appRibbon :: Maybe Text
|
||||||
} deriving Show
|
} deriving Show
|
||||||
@ -501,6 +506,11 @@ instance FromJSON AppSettings where
|
|||||||
appSessionTokenExpiration <- o .:? "session-token-expiration"
|
appSessionTokenExpiration <- o .:? "session-token-expiration"
|
||||||
appSessionTokenEncoding <- o .: "session-token-encoding"
|
appSessionTokenEncoding <- o .: "session-token-encoding"
|
||||||
|
|
||||||
|
appFavouritesQuickActionsBurstsize <- o .: "favourites-quick-actions-burstsize"
|
||||||
|
appFavouritesQuickActionsAvgInverseRate <- o .: "favourites-quick-actions-avg-inverse-rate"
|
||||||
|
appFavouritesQuickActionsTimeout <- o .: "favourites-quick-actions-timeout"
|
||||||
|
appFavouritesQuickActionsCacheTTL <- o .: "favourites-quick-actions-cache-ttl"
|
||||||
|
|
||||||
return AppSettings{..}
|
return AppSettings{..}
|
||||||
|
|
||||||
makeClassy_ ''AppSettings
|
makeClassy_ ''AppSettings
|
||||||
|
|||||||
@ -5,6 +5,7 @@ module Utils.Metrics
|
|||||||
, registerReadyMetric
|
, registerReadyMetric
|
||||||
, withJobWorkerStateLbls
|
, withJobWorkerStateLbls
|
||||||
, observeYesodCacheSize
|
, observeYesodCacheSize
|
||||||
|
, observeFavouritesQuickActionsDuration
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoFoundation hiding (Vector, Info)
|
import Import.NoFoundation hiding (Vector, Info)
|
||||||
@ -91,6 +92,13 @@ yesodCacheSize = unsafeRegister $ histogram info buckets
|
|||||||
"Number of items in Yesod's ghsCache and ghsCacheBy"
|
"Number of items in Yesod's ghsCache and ghsCacheBy"
|
||||||
buckets = 0 : histogramBuckets 1 1e6
|
buckets = 0 : histogramBuckets 1 1e6
|
||||||
|
|
||||||
|
{-# NOINLINE favouritesQuickActionsDuration #-}
|
||||||
|
favouritesQuickActionsDuration :: Histogram
|
||||||
|
favouritesQuickActionsDuration = unsafeRegister $ histogram info buckets
|
||||||
|
where info = Info "uni2work_favourites_quick_actions_seconds"
|
||||||
|
"Duration of time needed to calculate a set of favourite quick actions"
|
||||||
|
buckets = histogramBuckets 500e-6 50
|
||||||
|
|
||||||
|
|
||||||
withHealthReportMetrics :: MonadIO m => m HealthReport -> m HealthReport
|
withHealthReportMetrics :: MonadIO m => m HealthReport -> m HealthReport
|
||||||
withHealthReportMetrics act = do
|
withHealthReportMetrics act = do
|
||||||
@ -152,3 +160,13 @@ observeYesodCacheSize = do
|
|||||||
GHState{..} <- readIORef handlerState
|
GHState{..} <- readIORef handlerState
|
||||||
let size = fromIntegral $ length ghsCache + length ghsCacheBy
|
let size = fromIntegral $ length ghsCache + length ghsCacheBy
|
||||||
observe yesodCacheSize size
|
observe yesodCacheSize size
|
||||||
|
|
||||||
|
observeFavouritesQuickActionsDuration :: (MonadIO m, MonadMask m) => m a -> m a
|
||||||
|
observeFavouritesQuickActionsDuration act = do
|
||||||
|
start <- liftIO getPOSIXTime
|
||||||
|
res <- handleAll (return . Left) $ Right <$> act
|
||||||
|
end <- liftIO getPOSIXTime
|
||||||
|
|
||||||
|
liftIO . observe favouritesQuickActionsDuration . realToFrac $ end - start
|
||||||
|
|
||||||
|
either throwM return res
|
||||||
|
|||||||
@ -6,6 +6,7 @@ module Utils.PathPiece
|
|||||||
, nameToPathPiece, nameToPathPiece'
|
, nameToPathPiece, nameToPathPiece'
|
||||||
, tuplePathPiece
|
, tuplePathPiece
|
||||||
, pathPieceJSON, pathPieceJSONKey
|
, pathPieceJSON, pathPieceJSONKey
|
||||||
|
, pathPieceBinary
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude.Yesod
|
import ClassyPrelude.Yesod
|
||||||
@ -31,6 +32,9 @@ import qualified Data.Aeson.Types as Aeson
|
|||||||
|
|
||||||
import Control.Monad.Fail
|
import Control.Monad.Fail
|
||||||
|
|
||||||
|
import Data.Binary (Binary)
|
||||||
|
import qualified Data.Binary as Binary
|
||||||
|
|
||||||
|
|
||||||
mkFiniteFromPathPiece :: Name -> Q ([Dec], Exp)
|
mkFiniteFromPathPiece :: Name -> Q ([Dec], Exp)
|
||||||
mkFiniteFromPathPiece finiteType = do
|
mkFiniteFromPathPiece finiteType = do
|
||||||
@ -159,3 +163,10 @@ pathPieceJSON tName
|
|||||||
instance FromJSON $(conT tName) where
|
instance FromJSON $(conT tName) where
|
||||||
parseJSON = Aeson.withText $(TH.lift $ nameBase tName) $ \t -> maybe (fail $ "Could not parse ‘" <> unpack t <> "’ as value for " <> $(TH.lift $ nameBase tName) <> " via PathPiece") return $ fromPathPiece t
|
parseJSON = Aeson.withText $(TH.lift $ nameBase tName) $ \t -> maybe (fail $ "Could not parse ‘" <> unpack t <> "’ as value for " <> $(TH.lift $ nameBase tName) <> " via PathPiece") return $ fromPathPiece t
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
pathPieceBinary :: Name -> DecsQ
|
||||||
|
pathPieceBinary tName
|
||||||
|
= [d| instance Binary $(conT tName) where
|
||||||
|
get = Binary.get >>= maybe (fail $ "Could not parse value of " <> $(TH.lift $ nameBase tName) <> " via PathPiece") return . fromPathPiece
|
||||||
|
put = Binary.put . toPathPiece
|
||||||
|
|]
|
||||||
|
|||||||
@ -21,16 +21,20 @@ $newline never
|
|||||||
<h3 .asidenav__box-subtitle>
|
<h3 .asidenav__box-subtitle>
|
||||||
_{favReason}
|
_{favReason}
|
||||||
<ul .asidenav__list.list--iconless>
|
<ul .asidenav__list.list--iconless>
|
||||||
$forall (Course{courseShorthand, courseName}, courseRoute, pageActions, _) <- favouriteTermReason tid favReason
|
$forall (Course{courseShorthand, courseName}, courseRoute, mPageActions, _) <- favouriteTermReason tid favReason
|
||||||
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
|
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
|
||||||
<a .asidenav__link-wrapper href=@{courseRoute}>
|
<a .asidenav__link-wrapper href=@{courseRoute}>
|
||||||
<div .asidenav__link-shorthand>#{courseShorthand}
|
<div .asidenav__link-shorthand>#{courseShorthand}
|
||||||
<div .asidenav__link-label>#{courseName}
|
<div .asidenav__link-label>#{courseName}
|
||||||
<div .asidenav__nested-list-wrapper>
|
<div .asidenav__nested-list-wrapper>
|
||||||
<ul .asidenav__nested-list.list--iconless>
|
$maybe pageActions <- mPageActions
|
||||||
$forall (NavLink{navLabel}, route) <- pageActions
|
<ul .asidenav__nested-list.list--iconless>
|
||||||
<li .asidenav__nested-list-item>
|
$forall (label, route) <- pageActions
|
||||||
<a .asidenav__link-wrapper href=#{route}>_{navLabel}
|
<li .asidenav__nested-list-item>
|
||||||
|
<a .asidenav__link-wrapper href=#{route}>#{label}
|
||||||
|
$nothing
|
||||||
|
<p .asidenav__nested-list--unavailable>
|
||||||
|
_{MsgFavouritesUnavailableTip}
|
||||||
|
|
||||||
<div .asidenav__sigillum>
|
<div .asidenav__sigillum>
|
||||||
<img src=@{StaticR img_lmu_sigillum_svg}>
|
<img src=@{StaticR img_lmu_sigillum_svg}>
|
||||||
|
|||||||
Reference in New Issue
Block a user