diff --git a/hledger-web/Hledger/Web/App.hs b/hledger-web/Hledger/Web/App.hs index 207f26e05..074edffe5 100644 --- a/hledger-web/Hledger/Web/App.hs +++ b/hledger-web/Hledger/Web/App.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, TypeFamilies, GeneralizedNewtypeDeriving #-} +{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, TypeFamilies, GeneralizedNewtypeDeriving, OverloadedStrings #-} {-# LANGUAGE TypeSynonymInstances, FlexibleInstances, MultiParamTypeClasses, FlexibleContexts #-} {-| The web app providing a richer interface to hledger's data. @@ -11,11 +11,14 @@ module Hledger.Web.App where import Control.Applicative ((<$>), (<*>)) -- import Control.Failure --- import qualified Data.ByteString.Lazy as L +import qualified Data.ByteString.Lazy as L import Data.Either --- import System.Directory +-- import qualified Data.Text as T +import Data.Text(Text,pack,unpack) +import System.Directory import System.FilePath ((), takeFileName) import System.IO.Storage (putValue, getValue) +import Text.Jasmine (minifym) import Text.ParserCombinators.Parsec hiding (string) -- import Database.Persist.GenericSql (ConnectionPool, SqlPersist, runMigration, migrate) @@ -37,24 +40,24 @@ import Hledger.Cli.Version (version) import Hledger.Data hiding (insert, today) import Hledger.Read (journalFromPathAndString) import Hledger.Read.JournalReader (someamount) -import Hledger.Web.Settings ( - -- withConnectionPool - -- , runConnectionPool - -- , staticroot - datadir - -- , hamletFile - -- , cassiusFile - -- , juliusFile - , hledgerorgurl - , manualurl - , style_css - , hledger_js - , jquery_js - , jquery_url_js - , dhtmlxcommon_js - , dhtmlxcombo_js - , robots_txt - ) +import Hledger.Web.Settings + -- -- withConnectionPool + -- -- , runConnectionPool + -- -- , staticroot + -- datadir + -- -- , hamletFile + -- -- , cassiusFile + -- -- , juliusFile + -- -- , hledgerorgurl + -- , manualurl + -- -- , style_css + -- -- , hledger_js + -- -- , jquery_js + -- -- , jquery_url_js + -- -- , dhtmlxcommon_js + -- -- , dhtmlxcombo_js + -- , robots_txt + -- ) ---------------------------------------------------------------------- @@ -80,12 +83,12 @@ import Hledger.Web.Settings ( -- run-time data kept by the web app. data App = App { -- appConnPool :: Maybe ConnectionPool - appRoot :: String + appRoot :: Text ,appDataDir :: FilePath ,appOpts :: [Opt] ,appArgs :: [String] + ,appStaticSettings :: Static ,appJournal :: Journal - ,appStatic :: Static } -- parseRoutes (quasi-quoter from web-routes) defines a list of route patterns for the web app. @@ -93,9 +96,9 @@ data App = App -- /auth AuthR Auth getAuth mkYesod "App" [$parseRoutes| / IndexR GET +/static StaticR Static appStaticSettings /favicon.ico FaviconR GET /robots.txt RobotsR GET -/static StaticR Static appStatic /journalonly JournalOnlyR GET POST /registeronly RegisterOnlyR GET /accounts AccountsOnlyR GET @@ -108,6 +111,24 @@ type Handler = GHandler App App instance Yesod App where approot = appRoot + -- This function creates static content files in the static folder + -- and names them based on a hash of their content. This allows + -- expiration dates to be set far in the future without worry of + -- users receiving stale content. + addStaticContent ext' _ content = do + let fn = base64md5 content ++ '.' : unpack ext' + let content' = + if ext' == "js" + then case minifym content of + Left _ -> content + Right y -> y + else content + let statictmp = Hledger.Web.Settings.datadir ++ "/tmp/" + liftIO $ createDirectoryIfMissing True statictmp + let fn' = statictmp ++ fn + exists <- liftIO $ doesFileExist fn' + unless exists $ liftIO $ L.writeFile fn' content' + return $ Just $ Right (StaticR $ StaticRoute ["tmp", pack fn] [], []) -- defaultLayout widget = do -- mmsg <- getMessage @@ -230,6 +251,7 @@ withApp app f = toWaiApp app >>= f -- handler utilities, common templates ---------------------------------------------------------------------- +nulltemplate :: Hamlet AppRoute nulltemplate = [$hamlet||] -- | A bundle of useful data passed to templates. @@ -243,6 +265,7 @@ data TemplateData = TD { ,today :: Day -- ^ the current day } +mktd :: TemplateData mktd = TD { here = IndexR ,title = "hledger" @@ -260,11 +283,11 @@ mktd = TD { -- message if any. getHandlerData :: Handler (String, String, [Opt], FilterSpec, Journal, Maybe Html, AppRoute) getHandlerData = do - Just here <- getCurrentRoute + Just here' <- getCurrentRoute (a, p, opts, fspec) <- getReportParameters (j, err) <- getLatestJournal opts msg <- getMessage' err - return (a, p, opts, fspec, j, msg, here) + return (a, p, opts, fspec, j, msg, here') where -- | Get current report parameters for this request. getReportParameters :: Handler (String, String, [Opt], FilterSpec) @@ -273,10 +296,11 @@ getHandlerData = do t <- liftIO $ getCurrentLocalTime a <- fromMaybe "" <$> lookupGetParam "a" p <- fromMaybe "" <$> lookupGetParam "p" - let opts = appOpts app ++ [Period p] - args = appArgs app ++ words' a + let (a',p') = (unpack a, unpack p) + opts = appOpts app ++ [Period p'] + args = appArgs app ++ words' a' fspec = optsToFilterSpec opts args t - return (a, p, opts, fspec) + return (a', p', opts, fspec) -- | Quote-sensitive words, ie don't split on spaces which are inside quotes. words' :: String -> [String] @@ -297,52 +321,52 @@ getHandlerData = do else case jE of Right j' -> do liftIO $ putValue "hledger" "journal" j' return (j',Nothing) - Left e -> do setMessage $ string "error while reading" {- ++ ": " ++ e-} + Left e -> do setMessage $ "error while reading" {- ++ ": " ++ e-} return (j, Just e) -- | Helper to work around a yesod feature (can't set and get a message in the same request.) getMessage' :: Maybe String -> Handler (Maybe Html) getMessage' newmsgstr = do oldmsg <- getMessage - return $ maybe oldmsg (Just . string) newmsgstr + return $ maybe oldmsg (Just . toHtml) newmsgstr -- | Wrap a template with the standard hledger web ui page layout. pageLayout :: TemplateData -> Hamlet AppRoute -> Hamlet AppRoute pageLayout td@TD{title=basetitle, msg=msg, p=p, j=j, today=today} content = [$hamlet| !!! -%html - %head - %title $title'$ - %meta!http-equiv=Content-Type!content=$metacontent$ - %script!type=text/javascript!src=@StaticR.jquery_js@ - %script!type=text/javascript!src=@StaticR.jquery_url_js@ - %script!type=text/javascript!src=@StaticR.dhtmlxcommon_js@ - %script!type=text/javascript!src=@StaticR.dhtmlxcombo_js@ - %script!type=text/javascript!src=@StaticR.hledger_js@ - %link!rel=stylesheet!type=text/css!media=all!href=@StaticR.style_css@ - %body - ^navbar.td^ - #messages $m$ - #content - ^content^ +#{title'} + #{m} + Hamlet AppRoute navbar TD{p=p,j=j,today=today} = [$hamlet| - #navbar - %a.topleftlink!href=$hledgerorgurl$ + - $version$ - %a.toprightlink!href=$manualurl$!target=hledgerhelp manual - %h1 $title$ - \ $ - %span#journaldesc $desc$ + #{version} + #{title} + \ # + #{desc} |] where (title, desc) = journalTitleDesc j p today @@ -360,73 +384,73 @@ journalTitleDesc j p today = (title, desc) -- | Links to the main views. navlinks :: TemplateData -> Hamlet AppRoute navlinks td = [$hamlet| - #navlinks - ^accountsjournallink^ - \ | $ - ^accountsregisterlink^ - \ | $ - %a#addformlink!href!onclick="return addformToggle(event)" add transaction - %a#importformlink!href!onclick="return importformToggle(event)"!style=display:none; import transactions - \ | $ - %a#editformlink!href!onclick="return editformToggle(event)" edit journal + add transaction + import transactions + \ | # + edit journal |] --- \ | $ +-- \ | # where accountsjournallink = navlink td "journal" JournalR accountsregisterlink = navlink td "register" RegisterR navlink :: TemplateData -> String -> AppRoute -> Hamlet AppRoute -navlink TD{here=here,a=a,p=p} s dest = [$hamlet|%a#$s$link.$style$!href=@?u@ $s$|] - where u = (dest, concat [(if null a then [] else [("a", a)]) - ,(if null p then [] else [("p", p)])]) +navlink TD{here=here,a=a,p=p} s dest = [$hamlet|#{s}|] + where u = (dest, concat [(if null a then [] else [("a", pack a)]) + ,(if null p then [] else [("p", pack p)])]) style | dest == here = "navlinkcurrent" - | otherwise = "navlink" + | otherwise = "navlink" :: Text -- | Form controlling journal filtering parameters. filterform :: TemplateData -> Hamlet AppRoute filterform TD{here=here,a=a,p=p} = [$hamlet| - #filterformdiv - %form#filterform.form!method=GET!style=display:$visible$; - %table.form - %tr.$filteringperiodclass$ - %td + String -> Hamlet AppRoute -helplink topic label = [$hamlet|%a!href=$u$!target=hledgerhelp $label$|] +helplink topic label = [$hamlet|#{label}|] where u = manualurl ++ if null topic then "" else '#':topic {- @@ -521,17 +545,17 @@ getJournalR = do td = mktd{here=here, title="hledger journal", msg=msg, a=a, p=p, j=j, today=today} editform' = editform td hamletToRepHtml $ pageLayout td [$hamlet| - %div#content - %div#sidebar - ^sidecontent^ - %div#main.journal - ^navlinks.td^ - %div#transactions - ^filterform.td^ - ^maincontent^ - ^addform.td^ - ^editform'^ - ^importform^ + TemplateData -> BalanceReport -> Hamlet AppRoute balanceReportAsHtml _ td@TD{here=here,a=a,p=p} (items,total) = [$hamlet| -^accountsheading^ -%table.balancereport - $forall items i - ^itemAsHtml' i^ - %tr.totalrule - %td!colspan=2 - %tr - %td - %td!align=right $mixedAmountAsHtml.total$ +^{accountsheading} + + $forall i <- items + ^{itemAsHtml' i} + + + + + #{mixedAmountAsHtml total} > |] where accountsheading = [$hamlet| - #accountsheading + let a' = if isAccountRegex a then a else acct a'' = accountNameToAccountRegex $ parentAccountName $ accountRegexToAccountName a' - parenturl = (here, [("a",a''), ("p",p)]) + parenturl = (here, [("a",pack a''), ("p",pack p)]) in [$hamlet| - \ | $ - %a!href=@?parenturl@ show more ↑ + \ | # + nulltemplate showall = if filteringaccts then [$hamlet| - \ | $ - %a!href=@?allurl@ show all + \ | # + BalanceReportItem -> Hamlet AppRoute itemAsHtml TD{p=p} (acct, adisplay, adepth, abal) = [$hamlet| - %tr.item - %td.account - $indent$ - %a!href=$aurl$ $adisplay$ - %td.balance!align=right $mixedAmountAsHtml.abal$ + #{adisplay} + #{mixedAmountAsHtml abal} |] where -- current = if not (null a) && containsRegex a acct then "current" else "" indent = preEscapedString $ concat $ replicate (2 * adepth) " " aurl = printf ".?a=%s%s" (accountNameToAccountRegex acct) p' :: String - p' = if null p then "" else printf "&p=%s" p + p' = if null p then "" else printf "&p=%s" p :: String accountNameToAccountRegex :: String -> String accountNameToAccountRegex "" = "" @@ -656,39 +680,39 @@ getJournalOnlyR = do editform' = editform td txns = journalReportAsHtml opts td $ journalReport opts fspec j hamletToRepHtml $ pageLayout td [$hamlet| -%div#journal - %div.nav2 - %a#addformlink!href!onclick="return addformToggle(event)" add one transaction - \ | $ - %a#editformlink!href!onclick="return editformToggle(event)" edit the whole journal - #transactions ^txns^ - ^addform.td^ - ^editform'^ + TemplateData -> JournalReport -> Hamlet AppRoute journalReportAsHtml _ td items = [$hamlet| -%table.journalreport - $forall number.items i - ^itemAsHtml' i^ + + $forall i <- number items + ^{itemAsHtml' i} |] where number = zip [1..] itemAsHtml' = itemAsHtml td itemAsHtml :: TemplateData -> (Int, JournalReportItem) -> Hamlet AppRoute itemAsHtml _ (n, t) = [$hamlet| - %tr.item.$evenodd$ - %td.transaction - %pre $txn$ + + +
 #{txn}
      |] where
-       evenodd = if even n then "even" else "odd"
+       evenodd = if even n then "even" else "odd" :: String
        txn = trimnl $ showTransaction t where trimnl = reverse . dropWhile (=='\n') . reverse
 
 addform :: TemplateData -> Hamlet AppRoute
 addform td = [$hamlet|
-%script!type=text/javascript
- $$(document).ready(function() {
+