snaplet-actionlog-0.2.0.1: src/Snap/Snaplet/ActionLog/Resource.hs
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
module Snap.Snaplet.ActionLog.Resource
( actionLogR
, indexH
, showH
, actionLogSplices
, actionLogISplices
) where
------------------------------------------------------------------------------
import Blaze.ByteString.Builder
import Blaze.ByteString.Builder.Char8
import Control.Error
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Monoid
import qualified Data.Readable as R
import Data.Text (Text)
import qualified Data.Text as T
import Database.Persist
import Heist
import Heist.Compiled
import qualified Heist.Interpreted as I
import Snap
import Snap.Restful
import Snap.Snaplet.ActionLog.API
import Snap.Snaplet.ActionLog.Types
import Snap.Snaplet.Heist.Generic
import Snap.Snaplet.Persistent
import Text.Digestive
import qualified Text.Digestive as Form
import qualified Text.Digestive.Heist as DHI
import Text.Digestive.Heist.Compiled
import Text.Digestive.Snap hiding (method)
import qualified Text.Digestive.Snap as Form
import qualified Text.XmlHtml as X
------------------------------------------------------------------------------
snapletRender :: HasHeist b => ByteString -> Handler b v ()
snapletRender name = do
root <- getSnapletRootURL
let p = B.intercalate "/" $ filter (not . B.null) [root, name]
gRender p
indexH :: HasHeist b => Handler b v ()
indexH = snapletRender "index"
showH :: HasHeist b => Handler b v ()
showH = snapletRender "show"
-------------------------------------------------------------------------------
-- | A restful-snap resource for the action log CRUD.
actionLogR :: Resource
actionLogR = Resource
{ rName = "actionlog"
, rRoot = ""
, rResourceEndpoints = []
, rItemEndpoints = []
}
data LogFilter = LogFilter
{ filterUser :: Maybe Int
, filterEntity :: Maybe Text
, filterEntityId :: Maybe Int
, filterAction :: Maybe ActionType
} deriving (Show)
instance Monoid LogFilter where
mempty = LogFilter Nothing Nothing Nothing Nothing
mappend (LogFilter u1 e1 i1 a1) (LogFilter u2 e2 i2 a2) =
LogFilter (getFirst $ mappend (First u1) (First u2))
(getFirst $ mappend (First e1) (First e2))
(getFirst $ mappend (First i1) (First i2))
(getFirst $ mappend (First a1) (First a2))
mkFilters :: LogFilter -> [Filter LoggedAction]
mkFilters (LogFilter u e eid a) =
maybe [] (\x -> [LoggedActionUserId ==. x]) u ++
maybe [] (\x -> [LoggedActionEntityName ==. x]) e ++
maybe [] (\x -> [LoggedActionEntityId ==. x]) eid ++
maybe [] (\x -> [LoggedActionAction ==. x]) a
disableOnJust :: (Maybe a -> Form v m b) -> Maybe a -> Form v m b
disableOnJust f Nothing = f Nothing
disableOnJust f def = disable $ f def
------------------------------------------------------------------------------
-- |
logFilterForm :: HasActionLog m
=> Bool
-> Maybe LogFilter -> Form Text m LogFilter
logFilterForm isDisabling d = monadic $ do
entities <- getTenantEntities
let entityPairs = noFilter : map (\x -> (Just x,x)) entities
uids <- getTenantUids
names <- mapM alGetName uids
let userPairs = noFilter : (map firstJust $ zip uids names)
return $ LogFilter
<$> "user" .: choice userPairs ?$ (filterUser <$> d)
<*> "entity" .: choice entityPairs ?$ (filterEntity <$> d)
<*> "entity-id" .: optionalStringRead "id must be an int" ?$
(filterEntityId =<< d)
<*> "action" .: choice actions ?$ (filterAction <$> d)
where
noFilter = (Nothing, "Any")
firstJust (k,u) = (Just k, u)
actions = noFilter : map (\x -> (Just x,T.pack $ show x)) [minBound..maxBound]
-- An infix function here makes the syntax nice
infixr 6 ?$
(?$) :: (Maybe a -> Form v m b) -> Maybe a -> Form v m b
(?$) = if isDisabling then disableOnJust else ($)
logFilterFormName :: Text
logFilterFormName = "log-filter-form"
-------------------------------------------------------------------------------
runLogFilterForm :: (HasActionLog m, MonadSnap m)
=> Bool -> Maybe LogFilter -> m (View Text, Maybe LogFilter)
runLogFilterForm isDisabling def =
runFormWith cfg logFilterFormName (logFilterForm isDisabling def)
where
cfg = defaultSnapFormConfig { Form.method = Just Form.Post }
-------------------------------------------------------------------------------
-- Splices
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
actionLogSplices :: (HasActionLog n, MonadSnap n)
=> Resource -> [(Text, Splice n)]
actionLogSplices r =
[ ("actionDetails", actionViewSplice r)
, ("defaultActions", defaultActionsSplice r)
]
++ applyDeferred (return mempty) (coupledSplices r False)
coupledSplices :: (HasActionLog n, MonadSnap n)
=> Resource -> Bool -> [(Text, Promise LogFilter -> Splice n)]
coupledSplices r b =
[ ("actionlogListing", actionsSplice r (runLogFilterForm b))
, ("actionlogFilterForm", logFilterFormSplice (runLogFilterForm b))
]
getFilterFunc :: Monad n => HeistT n IO (RuntimeSplice n LogFilter)
getFilterFunc = do
n <- getParamNode
attrFunc <- runAttributesRaw $ X.elementAttrs n
return $ do
as <- attrFunc
return $ filterCommon as
filterCommon :: [(Text, Text)] -> LogFilter
filterCommon as =
LogFilter
(R.fromText =<< lookup "uid" as)
(lookup "entity" as)
(R.fromText =<< lookup "entity-id" as)
(R.fromText =<< lookup "action" as)
------------------------------------------------------------------------------
-- | This is a splice that wraps both the action log filter form splice and
-- the listing splice. It handles greying out the appropriate form fields and
-- limiting the things in the listing.
defaultActionsSplice :: (MonadSnap m, HasActionLog m) => Resource -> Splice m
defaultActionsSplice r = do
filterFunc <- getFilterFunc
withSplices runChildren (coupledSplices r True) filterFunc
actionFromId :: (MonadSnap m, HasPersistPool m)
=> m (Maybe (Entity LoggedAction))
actionFromId = runMaybeT $ do
idBS <- MaybeT $ getParam "id"
_id <- R.fromBS idBS
let key = mkKey _id
action <- MaybeT $ getLoggedAction key
return $ Entity key action
actionViewSplice :: (HasActionLog n, MonadSnap n) => Resource -> Splice n
actionViewSplice r = manyWithSplices runChildren (actionSplices r) $ do
ma <- lift actionFromId
return $ maybe [] (:[]) ma
actionsSplice :: HasActionLog n
=> Resource
-> (Maybe a -> n (t, Maybe LogFilter))
-> Promise LogFilter
-> Splice n
actionsSplice res form prom = manyWithSplices runChildren (actionSplices res) $ do
f <- getPromise prom
(_,r) <- lift $ form Nothing
let filters = case r of
Nothing -> []
Just lf -> mkFilters (f `mappend` lf)
lift $ getTenantActions filters []
applyDeferred :: Monad n
=> RuntimeSplice n a
-> [(Text, Promise a -> Splice n)]
-> [(Text, Splice n)]
applyDeferred m = applySnd m . mapSnd defer
actionSplices :: HasActionLog n
=> Resource
-> [(Text, Promise (Entity LoggedAction) -> Splice n)]
actionSplices r =
("loggedActionUserName", runtimeToPromise getUserName) :
("loggedActionDetails", detailsSplice) :
(pureSplices loggedActionCSplices ++
alCustomCSplices ++
repromise (return . DBId . mkWord64 . entityKey)
(pureSplices $ textSplices $ itemCSplices r)
)
where
getUserName = return . fromText <=< alGetName . loggedActionUserId . entityVal
detailsSplice prom =
manyWithSplices runChildren (pureSplices detailsCSplices)
(lift . getActionDetails . entityKey =<< getPromise prom)
runtimeToPromise :: (Monad n) => (t -> n Builder) -> Promise t -> Splice n
runtimeToPromise f p = return $ yieldRuntime $ do
entity <- getPromise p
lift $ f entity
-------------------------------------------------------------------------------
logFilterFormSplice :: Monad m
=> (Maybe a -> m (View Text, b))
-> Promise a
-> Splice m
logFilterFormSplice form prom =
formSplice [] [] $ do
f <- getPromise prom
lift $ liftM fst $ form (Just f)
-------------------------------------------------------------------------------
-- Interpreted
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
logFilterFormISplice :: MonadIO m
=> (Maybe a -> m (View Text, t))
-> a -> HeistT m m Template
logFilterFormISplice form f = do
(v,_) <- lift $ form (Just f)
localHS (DHI.bindDigestiveSplices v) (DHI.dfForm v >>= I.runNodeList)
--crudUrlISplice :: MonadSnap m => ByteString -> CRUD -> HeistT n m Template
--crudUrlISplice root crud =
-- I.textSplice . decodeUtf8 . (root -/-) =<< go crud
-- where
-- go RIndex = return ""
-- go RCreate = return ""
-- go RShow = (getParam "id") >>= maybe (go RIndex) return
-- go RNew = return "new"
-- go REdit = (getParam "id") >>= maybe (go RIndex) (return . (-/- "edit"))
-- go RUpdate = (getParam "id") >>= maybe (go RIndex) return
-- go RDestroy =
-- (getParam "id") >>= maybe (go RIndex) (return . (-/- "/destroy"))
--
--
--viewLinkISplice :: MonadSnap m => ByteString -> I.Splice m
--viewLinkISplice root = do
-- n <- getParamNode
-- case X.getAttribute "entity" n of
-- Nothing -> return []
-- Just e -> do
-- mid <- lift $ getParam "id"
-- case mid of
-- Nothing -> return []
-- Just _id -> do
-- let page = printf "%s?%s&%s" (kv "entity" e)
-- (kv "entity-id" (decodeUtf8 _id))
-- url = root -/- encodeUtf8 (T.pack page)
-- I.runChildrenWithText [("linkUrl", decodeUtf8 url)]
-- where
-- kv :: Text -> Text -> String
-- kv k v = printf "%s.%s=%s" (T.unpack logFilterFormName)
-- (T.unpack k) (T.unpack v)
-------------------------------------------------------------------------------
-- | Interpreted splice for an action log listing.
actionLogISplices :: (HasActionLog n, MonadSnap n)
=> Resource -> [(Text, I.Splice n)]
actionLogISplices r =
[ ("actionDetails", actionDetailsISplice r)
, ("defaultActions", defaultActionsISplice r)
]
++ coupledISplices r False mempty
coupledISplices :: (HasActionLog m, MonadSnap m)
=> Resource -> Bool -> LogFilter -> [(Text, I.Splice m)]
coupledISplices r b f =
[ ("actionlogListing", actionLogListingISplice r (runLogFilterForm b) f)
, ("actionlogFilterForm", logFilterFormISplice (runLogFilterForm b) f)
]
actionDetailsISplice :: (HasActionLog n, MonadSnap n)
=> Resource -> I.Splice n
actionDetailsISplice r = do
ma <- lift $ actionFromId
maybe (return []) (I.runChildrenWith . actionISplices r) ma
actionLogListingISplice :: HasActionLog m
=> Resource
-> (Maybe a -> m (t, Maybe LogFilter))
-> LogFilter
-> I.Splice m
actionLogListingISplice res form f = do
(_,r) <- lift $ form Nothing
let filters = case r of
Nothing -> []
Just lf -> mkFilters (f `mappend` lf)
actions <- lift $ getTenantActions filters []
I.mapSplices (I.runChildrenWith . actionISplices res) actions
actionISplices :: HasActionLog m
=> Resource
-> Entity LoggedAction
-> [(Text, I.Splice m)]
actionISplices r e =
("loggedActionUserName", I.textSplice =<< getUserName) :
("loggedActionDetails", detailsISplice) :
(loggedActionISplices (entityVal e) ++
alCustomISplices e ++
itemSplices r (DBId $ mkWord64 $ entityKey e)
)
where
getUserName = lift $ alGetName $ loggedActionUserId $ entityVal e
detailsISplice = do
ds <- lift $ getActionDetails $ entityKey e
I.mapSplices (I.runChildrenWith . detailsISplices . entityVal) ds
------------------------------------------------------------------------------
-- | This is a splice that wraps both the action log filter form splice and
-- the listing splice. It handles greying out the appropriate form fields and
-- limiting the things in the listing.
defaultActionsISplice :: (MonadSnap m, HasActionLog m)
=> Resource -> I.Splice m
defaultActionsISplice r = do
n <- getParamNode
let f = filterCommon $ X.elementAttrs n
I.runChildrenWith $ coupledISplices r True f