{-# 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