web: update for cassius, julius, template reloading, authentication, persistence
This commit is contained in:
		
							parent
							
								
									d132f5e45a
								
							
						
					
					
						commit
						63531f8adc
					
				| @ -1,21 +1,30 @@ | |||||||
| {-# LANGUAGE CPP, TypeFamilies, QuasiQuotes, TemplateHaskell #-} | {-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, TypeFamilies, GeneralizedNewtypeDeriving #-} | ||||||
| {-# LANGUAGE TypeSynonymInstances, FlexibleInstances, MultiParamTypeClasses, FlexibleContexts #-} | {-# LANGUAGE TypeSynonymInstances, FlexibleInstances, MultiParamTypeClasses, FlexibleContexts #-} | ||||||
| {-|  | {-|  | ||||||
| A web-based UI. | The web app providing a richer interface to hledger's data, along with | ||||||
|  | authentication, registration and persistent storage of user accounts. | ||||||
| -} | -} | ||||||
| 
 | module Hledger.Web.App | ||||||
| module Hledger.Web |     ( App (..) | ||||||
|  |     , withApp | ||||||
|  |     ) | ||||||
| where | where | ||||||
| import Control.Concurrent (forkIO, threadDelay) |  | ||||||
| import Control.Applicative ((<$>), (<*>)) | import Control.Applicative ((<$>), (<*>)) | ||||||
| import Control.Failure | import Control.Failure | ||||||
|  | -- import qualified Data.ByteString.Lazy as L | ||||||
| import Data.Either | import Data.Either | ||||||
|  | -- import System.Directory | ||||||
| import System.FilePath ((</>), takeFileName) | import System.FilePath ((</>), takeFileName) | ||||||
| import System.IO.Storage (withStore, putValue, getValue) | import System.IO.Storage (putValue, getValue) | ||||||
| import Text.ParserCombinators.Parsec (parse) | import Text.ParserCombinators.Parsec (parse) | ||||||
|  | 
 | ||||||
|  | import Database.Persist.GenericSql (ConnectionPool, SqlPersist, runMigration, migrate) | ||||||
| import Yesod | import Yesod | ||||||
|  | import Yesod.Mail | ||||||
| import Yesod.Helpers.Static | import Yesod.Helpers.Static | ||||||
| import Text.Hamlet | import Yesod.Helpers.Auth | ||||||
|  | -- import Yesod.WebRoutes | ||||||
|  | import Text.Hamlet (defaultHamletSettings) | ||||||
| import Text.Hamlet.RT | import Text.Hamlet.RT | ||||||
| 
 | 
 | ||||||
| import Hledger.Cli.Commands.Add (journalAddTransaction) | import Hledger.Cli.Commands.Add (journalAddTransaction) | ||||||
| @ -25,34 +34,66 @@ import Hledger.Cli.Commands.Register | |||||||
| import Hledger.Cli.Options hiding (value) | import Hledger.Cli.Options hiding (value) | ||||||
| import Hledger.Cli.Utils | import Hledger.Cli.Utils | ||||||
| import Hledger.Cli.Version (version) | import Hledger.Cli.Version (version) | ||||||
| import Hledger.Data hiding (today) | import Hledger.Data hiding (insert, today) | ||||||
| import Hledger.Read (journalFromPathAndString) | import Hledger.Read (journalFromPathAndString) | ||||||
| import Hledger.Read.Journal (someamount) | import Hledger.Read.Journal (someamount) | ||||||
| #ifdef MAKE | import Hledger.Web.Settings ( | ||||||
| import Paths_hledger_web_make (getDataFileName) |       withConnectionPool | ||||||
| #else |     , runConnectionPool | ||||||
| import Paths_hledger_web (getDataFileName) |     -- , staticroot | ||||||
| #endif |     , staticdir | ||||||
|  |     , hamletFile | ||||||
|  |     , cassiusFile | ||||||
|  |     , juliusFile | ||||||
|  |     , hledgerorgurl | ||||||
|  |     , manualurl | ||||||
|  |     , style_css | ||||||
|  |     , hledger_js | ||||||
|  |     , jquery_js | ||||||
|  |     , jquery_url_js | ||||||
|  |     , dhtmlxcommon_js | ||||||
|  |     , dhtmlxcombo_js | ||||||
|  |     , robots_txt | ||||||
|  |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| defhost           = "localhost" | ---------------------------------------------------------------------- | ||||||
| defport           = 5000 | -- define the web app | ||||||
| browserstartdelay = 100000 -- microseconds | ---------------------------------------------------------------------- | ||||||
| hledgerorgurl     = "http://hledger.org" |  | ||||||
| manualurl         = hledgerorgurl++"/MANUAL.html" |  | ||||||
| 
 | 
 | ||||||
| data HledgerWebApp = HledgerWebApp { | -- persistent data schema for the web app. User account info is stored here, | ||||||
|       appRoot    :: String | -- hledger's main data is stored in the usual places (journal files etc.) | ||||||
|      ,appDir     :: FilePath | -- persist (quasi-quoter from persistent) defines a list of data entities. | ||||||
|      ,appOpts    :: [Opt] | -- mkPersist (template haskell from persistent) defines persistence-capable data types based on these. | ||||||
|      ,appArgs    :: [String] | mkPersist [$persist| | ||||||
|      ,appJournal :: Journal | User | ||||||
|      ,appStatic  :: Static |     ident String | ||||||
|      } |     password String null update | ||||||
|  |     UniqueUser ident | ||||||
|  | Email | ||||||
|  |     email String | ||||||
|  |     user UserId null update | ||||||
|  |     verkey String null update | ||||||
|  |     UniqueEmail email | ||||||
|  | |] | ||||||
| 
 | 
 | ||||||
| type Handler = GHandler HledgerWebApp HledgerWebApp | -- run-time data kept by the web app. | ||||||
|  | data App = App | ||||||
|  |     {appConnPool :: Maybe ConnectionPool | ||||||
|  |     ,appRoot    :: String | ||||||
|  |     ,appDataDir :: FilePath | ||||||
|  |     ,appOpts    :: [Opt] | ||||||
|  |     ,appArgs    :: [String] | ||||||
|  |     ,appJournal :: Journal | ||||||
|  |     ,appStatic  :: Static | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| mkYesod "HledgerWebApp" [$parseRoutes| | -- parseRoutes (quasi-quoter from web-routes) defines a list of route patterns for the web app. | ||||||
|  | -- mkYesod (template haskell from yesod) defines types for the web app based on the routes. | ||||||
|  | mkYesod "App" [$parseRoutes| | ||||||
|  | /auth            AuthR             Auth getAuth | ||||||
|  | /favicon.ico     FaviconR          GET | ||||||
|  | /robots.txt      RobotsR           GET | ||||||
| /static          StaticR           Static appStatic | /static          StaticR           Static appStatic | ||||||
| /                IndexR            GET | /                IndexR            GET | ||||||
| /journalonly     JournalOnlyR      GET POST | /journalonly     JournalOnlyR      GET POST | ||||||
| @ -63,20 +104,132 @@ mkYesod "HledgerWebApp" [$parseRoutes| | |||||||
| /addformrt       AddformRTR        GET | /addformrt       AddformRTR        GET | ||||||
| |] | |] | ||||||
| 
 | 
 | ||||||
| style_css       = StaticRoute ["style.css"] [] | type Handler = GHandler App App | ||||||
| hledger_js      = StaticRoute ["hledger.js"] [] |  | ||||||
| jquery_js       = StaticRoute ["jquery.js"] [] |  | ||||||
| jquery_url_js   = StaticRoute ["jquery.url.js"] [] |  | ||||||
| dhtmlxcommon_js = StaticRoute ["dhtmlxcommon.js"] [] |  | ||||||
| dhtmlxcombo_js  = StaticRoute ["dhtmlxcombo.js"] [] |  | ||||||
| 
 | 
 | ||||||
| instance Yesod HledgerWebApp where approot = appRoot | instance Yesod App where | ||||||
|  |     approot = appRoot | ||||||
|  |     defaultLayout widget = do | ||||||
|  |         mmsg <- getMessage | ||||||
|  |         pc <- widgetToPageContent $ do | ||||||
|  |             widget | ||||||
|  |             addStyle $(cassiusFile "default-layout") | ||||||
|  |         hamletToRepHtml $(hamletFile "default-layout") | ||||||
|  |     authRoute _ = Just $ AuthR LoginR | ||||||
|  |     -- static file-serving optimisations, disable for the moment | ||||||
|  |     -- urlRenderOverride a (StaticR s) = | ||||||
|  |     --     Just $ uncurry (joinPath a staticroot) $ format s | ||||||
|  |     --   where | ||||||
|  |     --     format = formatPathSegments ss | ||||||
|  |     --     ss :: Site StaticRoute (String -> Maybe (GHandler Static App ChooseRep)) | ||||||
|  |     --     ss = getSubSite | ||||||
|  |     urlRenderOverride _ _ = Nothing | ||||||
|  |     -- addStaticContent ext' _ content = do | ||||||
|  |     --     let fn = base64md5 content ++ '.' : ext' | ||||||
|  |     --     let statictmp = staticdir ++ "/tmp/" | ||||||
|  |     --     liftIO $ createDirectoryIfMissing True statictmp | ||||||
|  |     --     liftIO $ L.writeFile (statictmp ++ fn) content | ||||||
|  |     --     return $ Just $ Right (StaticR $ StaticRoute ["tmp", fn] [], []) | ||||||
| 
 | 
 | ||||||
| defaultroute = JournalR | instance YesodPersist App where | ||||||
|  |     type YesodDB App = SqlPersist | ||||||
|  |     runDB db = do | ||||||
|  |       y <- getYesod | ||||||
|  |       let p = appConnPool y | ||||||
|  |       case p of Just p' -> runConnectionPool db p' | ||||||
|  |                 Nothing -> error "no connection pool, programmer error" -- XXX | ||||||
|  | 
 | ||||||
|  | instance YesodAuth App where | ||||||
|  |     type AuthEntity App = User | ||||||
|  |     type AuthEmailEntity App = Email | ||||||
|  | 
 | ||||||
|  |     defaultDest _ = IndexR | ||||||
|  | 
 | ||||||
|  |     getAuthId creds _extra = runDB $ do | ||||||
|  |         x <- getBy $ UniqueUser $ credsIdent creds | ||||||
|  |         case x of | ||||||
|  |             Just (uid, _) -> return $ Just uid | ||||||
|  |             Nothing -> do | ||||||
|  |                 fmap Just $ insert $ User (credsIdent creds) Nothing | ||||||
|  | 
 | ||||||
|  |     openIdEnabled _ = True | ||||||
|  | 
 | ||||||
|  |     emailSettings _ = Just EmailSettings { | ||||||
|  |           addUnverified = \email verkey -> runDB $ insert $ Email email Nothing (Just verkey) | ||||||
|  |         , sendVerifyEmail = sendVerifyEmail' | ||||||
|  |         , getVerifyKey = runDB . fmap (join . fmap emailVerkey) . get | ||||||
|  |         , setVerifyKey = \eid key -> runDB $ update eid [EmailVerkey $ Just key] | ||||||
|  |         , verifyAccount = \eid -> runDB $ do | ||||||
|  |             me <- get eid | ||||||
|  |             case me of | ||||||
|  |                 Nothing -> return Nothing | ||||||
|  |                 Just e -> do | ||||||
|  |                     let email = emailEmail e | ||||||
|  |                     case emailUser e of | ||||||
|  |                         Just uid -> return $ Just uid | ||||||
|  |                         Nothing -> do | ||||||
|  |                             uid <- insert $ User email Nothing | ||||||
|  |                             update eid [EmailUser $ Just uid] | ||||||
|  |                             return $ Just uid | ||||||
|  |         , getPassword = runDB . fmap (join . fmap userPassword) . get | ||||||
|  |         , setPassword = \uid pass -> runDB $ update uid [UserPassword $ Just pass] | ||||||
|  |         , getEmailCreds = \email -> runDB $ do | ||||||
|  |             me <- getBy $ UniqueEmail email | ||||||
|  |             case me of | ||||||
|  |                 Nothing -> return Nothing | ||||||
|  |                 Just (eid, e) -> return $ Just EmailCreds | ||||||
|  |                     { emailCredsId = eid | ||||||
|  |                     , emailCredsAuthId = emailUser e | ||||||
|  |                     , emailCredsStatus = isJust $ emailUser e | ||||||
|  |                     , emailCredsVerkey = emailVerkey e | ||||||
|  |                     } | ||||||
|  |         , getEmail = runDB . fmap (fmap emailEmail) . get | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | sendVerifyEmail' :: String -> String -> String -> GHandler Auth m () | ||||||
|  | sendVerifyEmail' email _ verurl = | ||||||
|  |     liftIO $ renderSendMail Mail | ||||||
|  |             { mailHeaders = | ||||||
|  |                 [ ("From", "noreply") | ||||||
|  |                 , ("To", email) | ||||||
|  |                 , ("Subject", "Verify your email address") | ||||||
|  |                 ] | ||||||
|  |             , mailPlain = verurl | ||||||
|  |             , mailParts = return Part | ||||||
|  |                 { partType = "text/html; charset=utf-8" | ||||||
|  |                 , partEncoding = None | ||||||
|  |                 , partDisposition = Inline | ||||||
|  |                 , partContent = renderHamlet id [$hamlet| | ||||||
|  | %p Please confirm your email address by clicking on the link below. | ||||||
|  | %p | ||||||
|  |     %a!href=$verurl$ $verurl$ | ||||||
|  | %p Thank you | ||||||
|  | |] | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  | -- | Migrate the app's persistent data and run the given yesod/persistent/wai-ish IO action on it. | ||||||
|  | withApp :: App -> (Yesod.Application -> IO a) -> IO a | ||||||
|  | withApp app f = toPersistentApp app >>= toWaiApp >>= f | ||||||
|  | 
 | ||||||
|  | -- | Obtain a persistent db connection pool to the app, and run any necessary data migrations. | ||||||
|  | toPersistentApp :: App -> IO App | ||||||
|  | toPersistentApp app = withConnectionPool $ \p -> do | ||||||
|  |     flip runConnectionPool p $ runMigration $ do | ||||||
|  |         migrate (undefined :: User) | ||||||
|  |         migrate (undefined :: Email) | ||||||
|  |         return () | ||||||
|  |     return app{appConnPool=Just p} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | -- handler utilities, common templates | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | 
 | ||||||
|  | nulltemplate = [$hamlet||] | ||||||
| 
 | 
 | ||||||
| -- | A bundle of useful data passed to templates. | -- | A bundle of useful data passed to templates. | ||||||
| data TemplateData = TD { | data TemplateData = TD { | ||||||
|      here         :: HledgerWebAppRoute -- ^ the current page's route |      here         :: AppRoute -- ^ the current page's route | ||||||
|     ,title        :: String             -- ^ page's title |     ,title        :: String             -- ^ page's title | ||||||
|     ,msg          :: Maybe Html         -- ^ transient message |     ,msg          :: Maybe Html         -- ^ transient message | ||||||
|     ,a            :: String             -- ^ a (acct/desc filter pattern) parameter |     ,a            :: String             -- ^ a (acct/desc filter pattern) parameter | ||||||
| @ -95,42 +248,13 @@ mktd = TD { | |||||||
|      ,today = ModifiedJulianDay 0 |      ,today = ModifiedJulianDay 0 | ||||||
|      } |      } | ||||||
| 
 | 
 | ||||||
| -- | The web command. | -- | Gather the data useful for a hledger web request handler, including: | ||||||
| web :: [Opt] -> [String] -> Journal -> IO () | -- initial command-line options, current a and p query string values, a | ||||||
| web opts args j = do | -- journal filter specification based on the above and the current time, | ||||||
|   let host    = defhost | -- an up-to-date parsed journal, the current route, and the current ui | ||||||
|       port    = fromMaybe defport $ portFromOpts opts | -- message if any. | ||||||
|       baseurl = fromMaybe (printf "http://%s:%d" host port) $ baseUrlFromOpts opts | getHandlerData :: Handler (String, String, [Opt], FilterSpec, Journal, Maybe Html, AppRoute) | ||||||
|   unless (Debug `elem` opts) $ forkIO (browser baseurl) >> return () | getHandlerData = do | ||||||
|   server baseurl port opts args j |  | ||||||
| 
 |  | ||||||
| browser :: String -> IO () |  | ||||||
| browser baseurl = do |  | ||||||
|   putStrLn "starting web browser" |  | ||||||
|   threadDelay browserstartdelay |  | ||||||
|   openBrowserOn baseurl |  | ||||||
|   return () |  | ||||||
| 
 |  | ||||||
| server :: String -> Int -> [Opt] -> [String] -> Journal -> IO () |  | ||||||
| server baseurl port opts args j = do |  | ||||||
|     printf "starting web server on port %d with base url %s\n" port baseurl |  | ||||||
|     dir <- getDataFileName "" |  | ||||||
|     let app = HledgerWebApp{ |  | ||||||
|                appRoot=baseurl |  | ||||||
|               ,appDir=dir |  | ||||||
|               ,appStatic=fileLookupDir (dir </> "static") $ typeByExt -- ++[("hamlet","text/plain")] |  | ||||||
|               ,appOpts=opts |  | ||||||
|               ,appArgs=args |  | ||||||
|               ,appJournal=j |  | ||||||
|               } |  | ||||||
|     withStore "hledger" $ do |  | ||||||
|      putValue "hledger" "journal" j |  | ||||||
|      basicHandler' port Nothing app |  | ||||||
| 
 |  | ||||||
| -- | Gather all the stuff we want for a typical hledger web request handler. |  | ||||||
| getHandlerParameters :: Handler |  | ||||||
|                        (String, String, [Opt], FilterSpec, Journal, Maybe Html, HledgerWebAppRoute) |  | ||||||
| getHandlerParameters = do |  | ||||||
|   Just here <- getCurrentRoute |   Just here <- getCurrentRoute | ||||||
|   (a, p, opts, fspec) <- getReportParameters |   (a, p, opts, fspec) <- getReportParameters | ||||||
|   (j, err) <- getLatestJournal opts |   (j, err) <- getLatestJournal opts | ||||||
| @ -170,18 +294,207 @@ getHandlerParameters = do | |||||||
|         oldmsg <- getMessage |         oldmsg <- getMessage | ||||||
|         return $ maybe oldmsg (Just . string) newmsgstr |         return $ maybe oldmsg (Just . string) 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^ | ||||||
|  | |] | ||||||
|  |  where title' = basetitle ++ " - " ++ journaltitle | ||||||
|  |        (journaltitle, _) = journalTitleDesc j p today | ||||||
|  |        metacontent = "text/html; charset=utf-8" | ||||||
|  |        m = fromMaybe (string "") msg | ||||||
|  | 
 | ||||||
|  | -- | Global toolbar/heading area. | ||||||
|  | navbar :: TemplateData -> Hamlet AppRoute | ||||||
|  | navbar TD{p=p,j=j,today=today} = [$hamlet| | ||||||
|  |  #navbar | ||||||
|  |   %a.topleftlink!href=$hledgerorgurl$ | ||||||
|  |    hledger | ||||||
|  |    <br /> | ||||||
|  |    $version$ | ||||||
|  |   %a.toprightlink!href=$manualurl$!target=hledgerhelp manual | ||||||
|  |   %h1 $title$ | ||||||
|  |   \ $ | ||||||
|  |   %span#journaldesc $desc$ | ||||||
|  | |] | ||||||
|  |   where (title, desc) = journalTitleDesc j p today | ||||||
|  | 
 | ||||||
|  | -- | Generate a title and description for the given journal, period | ||||||
|  | -- expression, and date. | ||||||
|  | journalTitleDesc :: Journal -> String -> Day -> (String, String) | ||||||
|  | journalTitleDesc j p today = (title, desc) | ||||||
|  |   where | ||||||
|  |     title = printf "%s" (takeFileName $ filepath j) :: String | ||||||
|  |     desc  = printf "%s" (showspan span) :: String | ||||||
|  |     span = either (const $ DateSpan Nothing Nothing) snd (parsePeriodExpr today p) | ||||||
|  |     showspan (DateSpan Nothing Nothing) = "" | ||||||
|  |     showspan s = " (" ++ dateSpanAsText s ++ ")" | ||||||
|  | 
 | ||||||
|  | -- | Links to the main views. | ||||||
|  | navlinks :: TemplateData -> Hamlet AppRoute | ||||||
|  | navlinks td = [$hamlet| | ||||||
|  |  #navlinks | ||||||
|  |   ^accountsjournallink^ | ||||||
|  |   \ | $ | ||||||
|  |   ^accountsregisterlink^ | ||||||
|  |   \ | $ | ||||||
|  |   %a#addformlink!href!onclick="return addformToggle()" add transaction | ||||||
|  |   %a#importformlink!href!onclick="return importformToggle()"!style=display:none; import transactions | ||||||
|  |   \ | $ | ||||||
|  |   %a#editformlink!href!onclick="return editformToggle()" 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)])]) | ||||||
|  |        style | dest == here = "navlinkcurrent" | ||||||
|  |              | otherwise    = "navlink" | ||||||
|  | 
 | ||||||
|  | -- | 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 | ||||||
|  |       filter by period: | ||||||
|  |       \ $ | ||||||
|  |      %td | ||||||
|  |       %input!name=p!size=60!value=$p$ | ||||||
|  |       ^phelp^ | ||||||
|  |       \ $ | ||||||
|  |      %td!align=right | ||||||
|  |       ^stopfilteringperiod^ | ||||||
|  |     %tr.$filteringclass$ | ||||||
|  |      %td | ||||||
|  |       filter by account/description: | ||||||
|  |       \ $ | ||||||
|  |      %td | ||||||
|  |       %input!name=a!size=60!value=$a$ | ||||||
|  |       ^ahelp^ | ||||||
|  |       \ $ | ||||||
|  |       %input!type=submit!value=filter $ | ||||||
|  |       \ $ | ||||||
|  |      %td!align=right | ||||||
|  |       ^stopfiltering^ | ||||||
|  | |] | ||||||
|  |  where | ||||||
|  |   ahelp = helplink "filter-patterns" "?" | ||||||
|  |   phelp = helplink "period-expressions" "?" | ||||||
|  |   filtering = not $ null a | ||||||
|  |   filteringperiod = not $ null p | ||||||
|  |   visible = "block" | ||||||
|  |   filteringclass = if filtering then "filtering" else "" | ||||||
|  |   filteringperiodclass = if filteringperiod then "filtering" else "" | ||||||
|  |   stopfiltering = if filtering then [$hamlet|%a#stopfilterlink!href=@?u@ stop filtering acct/desc|] else nulltemplate | ||||||
|  |       where u = (here, if filteringperiod then [("p", p)] else []) | ||||||
|  |   stopfilteringperiod = if filteringperiod then [$hamlet|%a#stopfilterlink!href=@?u@ stop filtering period|] else nulltemplate | ||||||
|  |       where u = (here, if filtering then [("a", a)] else []) | ||||||
|  | 
 | ||||||
|  | -- | Link to a topic in the manual. | ||||||
|  | helplink :: String -> String -> Hamlet AppRoute | ||||||
|  | helplink topic label = [$hamlet|%a!href=$u$!target=hledgerhelp $label$|] | ||||||
|  |     where u = manualurl ++ if null topic then "" else '#':topic | ||||||
|  | 
 | ||||||
|  | -- | Render a runtime template with the provided runtime data as html. | ||||||
|  | renderHamletFileRT :: FilePath -> HamletMap AppRoute -> Handler Html | ||||||
|  | renderHamletFileRT hfile hmap = do | ||||||
|  |   hrt <- readTemplateFile hfile >>= parseHamletRT defaultHamletSettings | ||||||
|  |   renderHamletRT hrt hmap urlParamsToString | ||||||
|  | 
 | ||||||
|  | -- | Read a file from the app's templates directory. | ||||||
|  | readTemplateFile :: FilePath -> Handler String | ||||||
|  | readTemplateFile hfile = do | ||||||
|  |   dir <- ((</> "templates") . appDataDir) `fmap` getYesod | ||||||
|  |   liftIO $ readFile $ dir </> hfile | ||||||
|  | 
 | ||||||
|  | -- what to do if rendering a runtime template fails. | ||||||
|  | instance Failure HamletException Handler | ||||||
|  |     where failure = error' . show | ||||||
|  | 
 | ||||||
|  | renderHamletAsHtml :: Hamlet AppRoute -> Html | ||||||
|  | renderHamletAsHtml h = h urlParamsToString | ||||||
|  | 
 | ||||||
|  | htmlAsHamlet :: Html -> Hamlet AppRoute | ||||||
|  | htmlAsHamlet h = [$hamlet|$h$|] | ||||||
|  | 
 | ||||||
|  | urlParamsToString :: AppRoute -> [(String,String)] -> String | ||||||
|  | urlParamsToString u [] = show u | ||||||
|  | urlParamsToString u ps = show u ++ "?" ++ intercalate "&" [k++"="++v | (k,v) <- ps] | ||||||
|  | 
 | ||||||
|  | -- | Convert a string to a hamlet HDHtml data item. | ||||||
|  | hdstring :: String -> HamletData AppRoute | ||||||
|  | hdstring = HDHtml . string | ||||||
|  | 
 | ||||||
|  | -- | Convert a simple list of strings to hamlet's complicated HDList type. | ||||||
|  | hdstringlist :: [String] -> HamletData AppRoute | ||||||
|  | hdstringlist ss = HDList [ [([], hdstring s)] | s <- ss ] | ||||||
|  | 
 | ||||||
|  | -- renderHamletRT' :: Failure HamletException m => HamletMap AppRoute -> HamletRT -> m Html | ||||||
|  | -- renderHamletRT' m h = renderHamletRT h m urlParamsToString | ||||||
|  | 
 | ||||||
|  | -- parseHamletRT' :: Failure HamletException m => String -> m HamletRT | ||||||
|  | -- parseHamletRT' s = parseHamletRT defaultHamletSettings s | ||||||
|  | 
 | ||||||
|  | -- hamletToHamletRT ::  Failure HamletException m => Hamlet AppRoute -> m HamletRT | ||||||
|  | -- hamletToHamletRT h = stringToHamletRT $ show $ unsafeByteString $ renderHamlet show h | ||||||
|  | 
 | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | -- handlers/views | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | 
 | ||||||
|  | getFaviconR :: Handler () | ||||||
|  | getFaviconR = sendFile "image/x-icon" $ staticdir </> "favicon.ico" | ||||||
|  | 
 | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | 
 | ||||||
|  | getRobotsR :: Handler RepPlain | ||||||
|  | getRobotsR = return $ RepPlain $ toContent robots_txt | ||||||
|  | 
 | ||||||
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||||||
| -- handlers & templates |  | ||||||
| 
 | 
 | ||||||
| getIndexR :: Handler () | getIndexR :: Handler () | ||||||
| getIndexR = redirect RedirectTemporary defaultroute | getIndexR = redirect RedirectTemporary defaultroute where defaultroute = JournalR | ||||||
|  | 
 | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | 
 | ||||||
|  | getDemoR :: Handler RepHtml | ||||||
|  | getDemoR = do | ||||||
|  |     mu <- maybeAuth | ||||||
|  |     defaultLayout $ do | ||||||
|  |         h2id <- newIdent | ||||||
|  |         setTitle $ string "hledger front page" | ||||||
|  |         addBody $(hamletFile "homepage") | ||||||
|  |         addStyle $(cassiusFile "homepage") | ||||||
|  |         addJavascript $(juliusFile "homepage") | ||||||
| 
 | 
 | ||||||
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
| -- | A combined accounts and journal view. | -- | A combined accounts and journal view. | ||||||
| getJournalR :: Handler RepHtml | getJournalR :: Handler RepHtml | ||||||
| getJournalR = do | getJournalR = do | ||||||
|   (a, p, opts, fspec, j, msg, here) <- getHandlerParameters |   (a, p, opts, fspec, j, msg, here) <- getHandlerData | ||||||
|   today <- liftIO getCurrentDay |   today <- liftIO getCurrentDay | ||||||
|   -- app <- getYesod |   -- app <- getYesod | ||||||
|   -- t <- liftIO $ getCurrentLocalTime |   -- t <- liftIO $ getCurrentLocalTime | ||||||
| @ -211,7 +524,7 @@ postJournalR = postJournalOnlyR | |||||||
| -- | A combined accounts and register view. | -- | A combined accounts and register view. | ||||||
| getRegisterR :: Handler RepHtml | getRegisterR :: Handler RepHtml | ||||||
| getRegisterR = do | getRegisterR = do | ||||||
|   (a, p, opts, fspec, j, msg, here) <- getHandlerParameters |   (a, p, opts, fspec, j, msg, here) <- getHandlerData | ||||||
|   today <- liftIO getCurrentDay |   today <- liftIO getCurrentDay | ||||||
|   -- app <- getYesod |   -- app <- getYesod | ||||||
|   -- t <- liftIO $ getCurrentLocalTime |   -- t <- liftIO $ getCurrentLocalTime | ||||||
| @ -242,13 +555,13 @@ postRegisterR = postJournalOnlyR | |||||||
| -- | A simple accounts and balances view like hledger balance. | -- | A simple accounts and balances view like hledger balance. | ||||||
| getAccountsOnlyR :: Handler RepHtml | getAccountsOnlyR :: Handler RepHtml | ||||||
| getAccountsOnlyR = do | getAccountsOnlyR = do | ||||||
|   (a, p, opts, fspec, j, msg, here) <- getHandlerParameters |   (a, p, opts, fspec, j, msg, here) <- getHandlerData | ||||||
|   today <- liftIO getCurrentDay |   today <- liftIO getCurrentDay | ||||||
|   let td = mktd{here=here, title="hledger accounts", msg=msg, a=a, p=p, j=j, today=today} |   let td = mktd{here=here, title="hledger accounts", msg=msg, a=a, p=p, j=j, today=today} | ||||||
|   hamletToRepHtml $ pageLayout td $ balanceReportAsHtml opts td $ balanceReport opts fspec j |   hamletToRepHtml $ pageLayout td $ balanceReportAsHtml opts td $ balanceReport opts fspec j | ||||||
| 
 | 
 | ||||||
| -- | Render a balance report as HTML. | -- | Render a balance report as HTML. | ||||||
| balanceReportAsHtml :: [Opt] -> TemplateData -> BalanceReport -> Hamlet HledgerWebAppRoute | balanceReportAsHtml :: [Opt] -> TemplateData -> BalanceReport -> Hamlet AppRoute | ||||||
| balanceReportAsHtml _ td@TD{here=here,a=a,p=p} (items,total) = [$hamlet| | balanceReportAsHtml _ td@TD{here=here,a=a,p=p} (items,total) = [$hamlet| | ||||||
| %table.balancereport | %table.balancereport | ||||||
|  %tr |  %tr | ||||||
| @ -291,7 +604,7 @@ balanceReportAsHtml _ td@TD{here=here,a=a,p=p} (items,total) = [$hamlet| | |||||||
|                     else nulltemplate |                     else nulltemplate | ||||||
|              where allurl = (here, [("p",p)]) |              where allurl = (here, [("p",p)]) | ||||||
|    itemAsHtml' = itemAsHtml td |    itemAsHtml' = itemAsHtml td | ||||||
|    itemAsHtml :: TemplateData -> BalanceReportItem -> Hamlet HledgerWebAppRoute |    itemAsHtml :: TemplateData -> BalanceReportItem -> Hamlet AppRoute | ||||||
|    itemAsHtml TD{p=p} (acct, adisplay, adepth, abal) = [$hamlet| |    itemAsHtml TD{p=p} (acct, adisplay, adepth, abal) = [$hamlet| | ||||||
|      %tr.item.$current$ |      %tr.item.$current$ | ||||||
|       %td.account |       %td.account | ||||||
| @ -319,7 +632,7 @@ isAccountRegex s = take 1 s == "^" && (take 5 $ reverse s) == ")$|:(" | |||||||
| -- | A basic journal view, like hledger print, with editing. | -- | A basic journal view, like hledger print, with editing. | ||||||
| getJournalOnlyR :: Handler RepHtml | getJournalOnlyR :: Handler RepHtml | ||||||
| getJournalOnlyR = do | getJournalOnlyR = do | ||||||
|   (a, p, opts, fspec, j, msg, here) <- getHandlerParameters |   (a, p, opts, fspec, j, msg, here) <- getHandlerData | ||||||
|   today <- liftIO getCurrentDay |   today <- liftIO getCurrentDay | ||||||
|   let td = mktd{here=here, title="hledger journal", msg=msg, a=a, p=p, j=j, today=today} |   let td = mktd{here=here, title="hledger journal", msg=msg, a=a, p=p, j=j, today=today} | ||||||
|       editform' = editform td $ jtext j |       editform' = editform td $ jtext j | ||||||
| @ -336,7 +649,7 @@ getJournalOnlyR = do | |||||||
| |] | |] | ||||||
| 
 | 
 | ||||||
| -- | Render a journal report as HTML. | -- | Render a journal report as HTML. | ||||||
| journalReportAsHtml :: [Opt] -> TemplateData -> JournalReport -> Hamlet HledgerWebAppRoute | journalReportAsHtml :: [Opt] -> TemplateData -> JournalReport -> Hamlet AppRoute | ||||||
| journalReportAsHtml _ td items = [$hamlet| | journalReportAsHtml _ td items = [$hamlet| | ||||||
| %table.journalreport | %table.journalreport | ||||||
|  $forall number.items i |  $forall number.items i | ||||||
| @ -345,7 +658,7 @@ journalReportAsHtml _ td items = [$hamlet| | |||||||
|  where |  where | ||||||
|    number = zip [1..] |    number = zip [1..] | ||||||
|    itemAsHtml' = itemAsHtml td |    itemAsHtml' = itemAsHtml td | ||||||
|    itemAsHtml :: TemplateData -> (Int, JournalReportItem) -> Hamlet HledgerWebAppRoute |    itemAsHtml :: TemplateData -> (Int, JournalReportItem) -> Hamlet AppRoute | ||||||
|    itemAsHtml _ (n, t) = [$hamlet| |    itemAsHtml _ (n, t) = [$hamlet| | ||||||
|      %tr.item.$evenodd$ |      %tr.item.$evenodd$ | ||||||
|       %td.transaction |       %td.transaction | ||||||
| @ -354,7 +667,7 @@ journalReportAsHtml _ td items = [$hamlet| | |||||||
|        evenodd = if even n then "even" else "odd" |        evenodd = if even n then "even" else "odd" | ||||||
|        txn = trimnl $ showTransaction t where trimnl = reverse . dropWhile (=='\n') . reverse |        txn = trimnl $ showTransaction t where trimnl = reverse . dropWhile (=='\n') . reverse | ||||||
| 
 | 
 | ||||||
| addform :: TemplateData -> Hamlet HledgerWebAppRoute | addform :: TemplateData -> Hamlet AppRoute | ||||||
| addform td = [$hamlet| | addform td = [$hamlet| | ||||||
| %script!type=text/javascript | %script!type=text/javascript | ||||||
|  $$(document).ready(function() { |  $$(document).ready(function() { | ||||||
| @ -409,7 +722,7 @@ addform td = [$hamlet| | |||||||
|   date = "today" |   date = "today" | ||||||
|   descriptions = sort $ nub $ map tdescription $ jtxns $ j td |   descriptions = sort $ nub $ map tdescription $ jtxns $ j td | ||||||
| 
 | 
 | ||||||
| postingsfields :: TemplateData -> Hamlet HledgerWebAppRoute | postingsfields :: TemplateData -> Hamlet AppRoute | ||||||
| postingsfields td = [$hamlet| | postingsfields td = [$hamlet| | ||||||
|  ^p1^ |  ^p1^ | ||||||
|  ^p2^ |  ^p2^ | ||||||
| @ -418,7 +731,7 @@ postingsfields td = [$hamlet| | |||||||
|     p1 = postingfields td 1 |     p1 = postingfields td 1 | ||||||
|     p2 = postingfields td 2 |     p2 = postingfields td 2 | ||||||
| 
 | 
 | ||||||
| postingfields :: TemplateData -> Int -> Hamlet HledgerWebAppRoute | postingfields :: TemplateData -> Int -> Hamlet AppRoute | ||||||
| postingfields td n = [$hamlet| | postingfields td n = [$hamlet| | ||||||
|  %tr#postingrow |  %tr#postingrow | ||||||
|   %td!align=right $acctlabel$: |   %td!align=right $acctlabel$: | ||||||
| @ -458,7 +771,7 @@ postingfields td n = [$hamlet| | |||||||
|                      ,"" |                      ,"" | ||||||
|                      ) |                      ) | ||||||
| 
 | 
 | ||||||
| editform :: TemplateData -> String -> Hamlet HledgerWebAppRoute | editform :: TemplateData -> String -> Hamlet AppRoute | ||||||
| editform _ content = [$hamlet| | editform _ content = [$hamlet| | ||||||
|  %form#editform!method=POST!style=display:none; |  %form#editform!method=POST!style=display:none; | ||||||
|   %table.form#editform |   %table.form#editform | ||||||
| @ -479,7 +792,7 @@ editform _ content = [$hamlet| | |||||||
|   where |   where | ||||||
|     formathelp = helplink "file-format" "file format help" |     formathelp = helplink "file-format" "file format help" | ||||||
| 
 | 
 | ||||||
| importform :: Hamlet HledgerWebAppRoute | importform :: Hamlet AppRoute | ||||||
| importform = [$hamlet| | importform = [$hamlet| | ||||||
|  %form#importform!method=POST!style=display:none; |  %form#importform!method=POST!style=display:none; | ||||||
|   %table.form |   %table.form | ||||||
| @ -502,7 +815,7 @@ postJournalOnlyR = do | |||||||
| -- | Handle a journal add form post. | -- | Handle a journal add form post. | ||||||
| postAddForm :: Handler RepPlain | postAddForm :: Handler RepPlain | ||||||
| postAddForm = do | postAddForm = do | ||||||
|   (_, _, opts, _, _, _, _) <- getHandlerParameters |   (_, _, opts, _, _, _, _) <- getHandlerData | ||||||
|   today <- liftIO getCurrentDay |   today <- liftIO getCurrentDay | ||||||
|   -- get form input values. M means a Maybe value. |   -- get form input values. M means a Maybe value. | ||||||
|   (dateM, descM, acct1M, amt1M, acct2M, amt2M) <- runFormPost' |   (dateM, descM, acct1M, amt1M, acct2M, amt2M) <- runFormPost' | ||||||
| @ -616,13 +929,13 @@ postImportForm = do | |||||||
| -- | A simple postings view like hledger register. | -- | A simple postings view like hledger register. | ||||||
| getRegisterOnlyR :: Handler RepHtml | getRegisterOnlyR :: Handler RepHtml | ||||||
| getRegisterOnlyR = do | getRegisterOnlyR = do | ||||||
|   (a, p, opts, fspec, j, msg, here) <- getHandlerParameters |   (a, p, opts, fspec, j, msg, here) <- getHandlerData | ||||||
|   today <- liftIO getCurrentDay |   today <- liftIO getCurrentDay | ||||||
|   let td = mktd{here=here, title="hledger register", msg=msg, a=a, p=p, j=j, today=today} |   let td = mktd{here=here, title="hledger register", msg=msg, a=a, p=p, j=j, today=today} | ||||||
|   hamletToRepHtml $ pageLayout td $ registerReportAsHtml opts td $ registerReport opts fspec j |   hamletToRepHtml $ pageLayout td $ registerReportAsHtml opts td $ registerReport opts fspec j | ||||||
| 
 | 
 | ||||||
| -- | Render a register report as HTML. | -- | Render a register report as HTML. | ||||||
| registerReportAsHtml :: [Opt] -> TemplateData -> RegisterReport -> Hamlet HledgerWebAppRoute | registerReportAsHtml :: [Opt] -> TemplateData -> RegisterReport -> Hamlet AppRoute | ||||||
| registerReportAsHtml _ td items = [$hamlet| | registerReportAsHtml _ td items = [$hamlet| | ||||||
| %table.registerreport | %table.registerreport | ||||||
|  %tr.headings |  %tr.headings | ||||||
| @ -640,7 +953,7 @@ registerReportAsHtml _ td items = [$hamlet| | |||||||
|                %th.balance!align=right Balance |                %th.balance!align=right Balance | ||||||
|                |] |                |] | ||||||
|    itemAsHtml' = itemAsHtml td |    itemAsHtml' = itemAsHtml td | ||||||
|    itemAsHtml :: TemplateData -> (Int, RegisterReportItem) -> Hamlet HledgerWebAppRoute |    itemAsHtml :: TemplateData -> (Int, RegisterReportItem) -> Hamlet AppRoute | ||||||
|    itemAsHtml TD{p=p} (n, (ds, posting, b)) = [$hamlet| |    itemAsHtml TD{p=p} (n, (ds, posting, b)) = [$hamlet| | ||||||
|      %tr.item.$evenodd$.$firstposting$ |      %tr.item.$evenodd$.$firstposting$ | ||||||
|       %td.date $date$ |       %td.date $date$ | ||||||
| @ -667,7 +980,7 @@ mixedAmountAsHtml b = preEscapedString $ addclass $ intercalate "<br>" $ lines $ | |||||||
| -- | A standalone journal edit form page. | -- | A standalone journal edit form page. | ||||||
| getEditR :: Handler RepHtml | getEditR :: Handler RepHtml | ||||||
| getEditR = do | getEditR = do | ||||||
|   (a, p, _, _, _, msg, here) <- getHandlerParameters |   (a, p, _, _, _, msg, here) <- getHandlerData | ||||||
|   today <- liftIO getCurrentDay |   today <- liftIO getCurrentDay | ||||||
|   -- reload journal's text without parsing, if changed     -- XXX are we doing this right ? |   -- reload journal's text without parsing, if changed     -- XXX are we doing this right ? | ||||||
|   j <- liftIO $ fromJust `fmap` getValue "hledger" "journal" |   j <- liftIO $ fromJust `fmap` getValue "hledger" "journal" | ||||||
| @ -678,10 +991,10 @@ getEditR = do | |||||||
| 
 | 
 | ||||||
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
| -- | Get the add form from template files reloaded at run-time. | -- | An add form with template files reloaded at runtime. | ||||||
| getAddformRTR :: Handler RepHtml | getAddformRTR :: Handler RepHtml | ||||||
| getAddformRTR = do | getAddformRTR = do | ||||||
|   (a, p, _, _, j, msg, here) <- getHandlerParameters |   (a, p, _, _, j, msg, here) <- getHandlerData | ||||||
|   today <- liftIO getCurrentDay |   today <- liftIO getCurrentDay | ||||||
|   let td = mktd{here=here, title="hledger add transaction", msg=msg, a=a, p=p, j=j, today=today} |   let td = mktd{here=here, title="hledger add transaction", msg=msg, a=a, p=p, j=j, today=today} | ||||||
|       descriptions = sort $ nub $ map tdescription $ jtxns j |       descriptions = sort $ nub $ map tdescription $ jtxns j | ||||||
| @ -690,10 +1003,10 @@ getAddformRTR = do | |||||||
|                        (["acctlabel"], hdstring acctlabel) |                        (["acctlabel"], hdstring acctlabel) | ||||||
|                       ,(["acctvar"],   hdstring acctvar) |                       ,(["acctvar"],   hdstring acctvar) | ||||||
|                       ,(["acctnames"], hdstringlist acctnames) |                       ,(["acctnames"], hdstringlist acctnames) | ||||||
|                       ,(["amtfield"],  HDHtml $ renderHamlet' amtfield) |                       ,(["amtfield"],  HDHtml $ renderHamletAsHtml amtfield) | ||||||
|                       ,(["accthelp"],  hdstring accthelp) |                       ,(["accthelp"],  hdstring accthelp) | ||||||
|                       ,(["amthelp"],   hdstring amthelp) |                       ,(["amthelp"],   hdstring amthelp) | ||||||
|                       ] :: HamletMap HledgerWebAppRoute |                       ] :: HamletMap AppRoute | ||||||
|           where |           where | ||||||
|             numbered = (++ show n) |             numbered = (++ show n) | ||||||
|             acctvar = numbered "account" |             acctvar = numbered "account" | ||||||
| @ -714,9 +1027,9 @@ getAddformRTR = do | |||||||
|                               ,nulltemplate |                               ,nulltemplate | ||||||
|                               ,"" |                               ,"" | ||||||
|                               ) |                               ) | ||||||
|   pfields1 <- renderHamletFile "addformpostingfields.hamlet" (postingData 1) |   pfields1 <- renderHamletFileRT "addformpostingfields.hamlet" (postingData 1) | ||||||
|   pfields2 <- renderHamletFile "addformpostingfields.hamlet" (postingData 2) |   pfields2 <- renderHamletFileRT "addformpostingfields.hamlet" (postingData 2) | ||||||
|   addform  <- renderHamletFile "addform.hamlet" ([ |   addform  <- renderHamletFileRT "addform.hamlet" ([ | ||||||
|                                                  (["date"], hdstring "today") |                                                  (["date"], hdstring "today") | ||||||
|                                                 ,(["desc"], hdstring "") |                                                 ,(["desc"], hdstring "") | ||||||
|                                                 ,(["descriptions"], hdstringlist descriptions) |                                                 ,(["descriptions"], hdstringlist descriptions) | ||||||
| @ -724,167 +1037,6 @@ getAddformRTR = do | |||||||
|                                                 ,(["deschelp"], hdstring "eg: supermarket (optional)") |                                                 ,(["deschelp"], hdstring "eg: supermarket (optional)") | ||||||
|                                                 ,(["postingfields1"], HDHtml pfields1) |                                                 ,(["postingfields1"], HDHtml pfields1) | ||||||
|                                                 ,(["postingfields2"], HDHtml pfields2) |                                                 ,(["postingfields2"], HDHtml pfields2) | ||||||
|                                                 ] :: HamletMap HledgerWebAppRoute) |                                                 ] :: HamletMap AppRoute) | ||||||
|   hamletToRepHtml $ pageLayout td $ htmlAsHamlet addform |   hamletToRepHtml $ pageLayout td $ htmlAsHamlet addform | ||||||
| 
 | 
 | ||||||
| -- | Convert a string to a hamlet HDHtml data item. |  | ||||||
| hdstring :: String -> HamletData HledgerWebAppRoute |  | ||||||
| hdstring = HDHtml . string |  | ||||||
| 
 |  | ||||||
| -- | Convert a simple list of strings to hamlet's complicated HDList type. |  | ||||||
| hdstringlist :: [String] -> HamletData HledgerWebAppRoute |  | ||||||
| hdstringlist ss = HDList [ [([], hdstring s)] | s <- ss ] |  | ||||||
| 
 |  | ||||||
| instance Failure HamletException Handler |  | ||||||
|     where failure = error' . show |  | ||||||
| 
 |  | ||||||
| renderHamletFile :: FilePath -> HamletMap HledgerWebAppRoute -> Handler Html |  | ||||||
| renderHamletFile hfile hmap = do |  | ||||||
|   hrt <- readHamletFile hfile >>= parseHamletRT defaultHamletSettings |  | ||||||
|   renderHamletRT hrt hmap renderurlwithparams |  | ||||||
| 
 |  | ||||||
| renderurlwithparams u [] = show u |  | ||||||
| renderurlwithparams u ps = show u ++ "?" ++ intercalate "&" [k++"="++v | (k,v) <- ps] |  | ||||||
| 
 |  | ||||||
| readHamletFile :: FilePath -> Handler String |  | ||||||
| readHamletFile hfile = do |  | ||||||
|   dir <- ((</> "templates") . appDir) `fmap` getYesod |  | ||||||
|   liftIO $ readFile $ dir </> hfile |  | ||||||
| 
 |  | ||||||
| htmlAsHamlet :: Html -> Hamlet HledgerWebAppRoute |  | ||||||
| htmlAsHamlet h = [$hamlet|$h$|] |  | ||||||
| 
 |  | ||||||
| parseHamletRT' :: Failure HamletException m => String -> m HamletRT |  | ||||||
| parseHamletRT' s = parseHamletRT defaultHamletSettings s |  | ||||||
| 
 |  | ||||||
| renderHamletRT' :: Failure HamletException m => HamletMap HledgerWebAppRoute -> HamletRT -> m Html |  | ||||||
| renderHamletRT' m h = renderHamletRT h m renderurlwithparams |  | ||||||
| 
 |  | ||||||
| renderHamlet' :: Hamlet HledgerWebAppRoute -> Html |  | ||||||
| renderHamlet' h = h renderurlwithparams |  | ||||||
| 
 |  | ||||||
| -- hamletToHamletRT ::  Failure HamletException m => Hamlet HledgerWebAppRoute -> m HamletRT |  | ||||||
| -- hamletToHamletRT h = stringToHamletRT $ show $ unsafeByteString $ renderHamlet show h |  | ||||||
| 
 |  | ||||||
| ---------------------------------------------------------------------- |  | ||||||
| 
 |  | ||||||
| -- | Wrap a template with the standard hledger web ui page layout. |  | ||||||
| pageLayout :: TemplateData -> Hamlet HledgerWebAppRoute -> Hamlet HledgerWebAppRoute |  | ||||||
| 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^ |  | ||||||
| |] |  | ||||||
|  where m = fromMaybe (string "") msg |  | ||||||
|        metacontent = "text/html; charset=utf-8" |  | ||||||
|        (journaltitle, _) = journalTitleInfo j p today |  | ||||||
|        title' = basetitle ++ " - " ++ journaltitle |  | ||||||
| 
 |  | ||||||
| navbar :: TemplateData -> Hamlet HledgerWebAppRoute |  | ||||||
| navbar TD{p=p,j=j,today=today} = [$hamlet| |  | ||||||
|  #navbar |  | ||||||
|   %a.topleftlink!href=$hledgerorgurl$ |  | ||||||
|    hledger |  | ||||||
|    <br /> |  | ||||||
|    $version$ |  | ||||||
|   %a.toprightlink!href=$manualurl$!target=hledgerhelp manual |  | ||||||
|   %h1 $journaltitle$ |  | ||||||
|   \ $ |  | ||||||
|   %span#journalinfo $journalinfo$ |  | ||||||
| |] |  | ||||||
|   where (journaltitle, journalinfo) = journalTitleInfo j p today |  | ||||||
| 
 |  | ||||||
| -- | Generate journal- and context-specific title and info strings for display. |  | ||||||
| journalTitleInfo :: Journal -> String -> Day -> (String, String) |  | ||||||
| journalTitleInfo j p today = (journaltitle, journalinfo) |  | ||||||
|   where |  | ||||||
|     journaltitle = printf "%s" (takeFileName $ filepath j) :: String |  | ||||||
|     journalinfo  = printf "%s" (showspan span) :: String |  | ||||||
|     span = either (const $ DateSpan Nothing Nothing) snd (parsePeriodExpr today p) |  | ||||||
|     showspan (DateSpan Nothing Nothing) = "" |  | ||||||
|     showspan s = " (" ++ dateSpanAsText s ++ ")" |  | ||||||
| 
 |  | ||||||
| navlinks :: TemplateData -> Hamlet HledgerWebAppRoute |  | ||||||
| navlinks td = [$hamlet| |  | ||||||
|  #navlinks |  | ||||||
|   ^accountsjournallink^ |  | ||||||
|   \ | $ |  | ||||||
|   ^accountsregisterlink^ |  | ||||||
|   \ | $ |  | ||||||
|   %a#addformlink!href!onclick="return addformToggle()" add transaction |  | ||||||
|   %a#importformlink!href!onclick="return importformToggle()"!style=display:none; import transactions |  | ||||||
|   \ | $ |  | ||||||
|   %a#editformlink!href!onclick="return editformToggle()" edit journal |  | ||||||
| |] |  | ||||||
| --  \ | $ |  | ||||||
|  where |  | ||||||
|    accountsjournallink  = navlink td "journal" JournalR |  | ||||||
|    accountsregisterlink = navlink td "register" RegisterR |  | ||||||
| 
 |  | ||||||
| navlink :: TemplateData -> String -> HledgerWebAppRoute -> Hamlet HledgerWebAppRoute |  | ||||||
| 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)])]) |  | ||||||
|        style | dest == here = "navlinkcurrent" |  | ||||||
|              | otherwise    = "navlink" |  | ||||||
| 
 |  | ||||||
| filterform :: TemplateData -> Hamlet HledgerWebAppRoute |  | ||||||
| filterform TD{here=here,a=a,p=p} = [$hamlet| |  | ||||||
|  #filterformdiv |  | ||||||
|   %form#filterform.form!method=GET!style=display:$visible$; |  | ||||||
|    %table.form |  | ||||||
|     %tr.$filteringperiodclass$ |  | ||||||
|      %td |  | ||||||
|       filter by period: |  | ||||||
|       \ $ |  | ||||||
|      %td |  | ||||||
|       %input!name=p!size=60!value=$p$ |  | ||||||
|       ^phelp^ |  | ||||||
|       \ $ |  | ||||||
|      %td!align=right |  | ||||||
|       ^stopfilteringperiod^ |  | ||||||
|     %tr.$filteringclass$ |  | ||||||
|      %td |  | ||||||
|       filter by account/description: |  | ||||||
|       \ $ |  | ||||||
|      %td |  | ||||||
|       %input!name=a!size=60!value=$a$ |  | ||||||
|       ^ahelp^ |  | ||||||
|       \ $ |  | ||||||
|       %input!type=submit!value=filter $ |  | ||||||
|       \ $ |  | ||||||
|      %td!align=right |  | ||||||
|       ^stopfiltering^ |  | ||||||
| |] |  | ||||||
|  where |  | ||||||
|   ahelp = helplink "filter-patterns" "?" |  | ||||||
|   phelp = helplink "period-expressions" "?" |  | ||||||
|   filtering = not $ null a |  | ||||||
|   filteringperiod = not $ null p |  | ||||||
|   visible = "block" |  | ||||||
|   filteringclass = if filtering then "filtering" else "" |  | ||||||
|   filteringperiodclass = if filteringperiod then "filtering" else "" |  | ||||||
|   stopfiltering = if filtering then [$hamlet|%a#stopfilterlink!href=@?u@ stop filtering acct/desc|] else nulltemplate |  | ||||||
|       where u = (here, if filteringperiod then [("p", p)] else []) |  | ||||||
|   stopfilteringperiod = if filteringperiod then [$hamlet|%a#stopfilterlink!href=@?u@ stop filtering period|] else nulltemplate |  | ||||||
|       where u = (here, if filtering then [("a", a)] else []) |  | ||||||
| 
 |  | ||||||
| helplink :: String -> String -> Hamlet HledgerWebAppRoute |  | ||||||
| helplink topic label = [$hamlet|%a!href=$u$!target=hledgerhelp $label$|] |  | ||||||
|     where u = manualurl ++ if null topic then "" else '#':topic |  | ||||||
| 
 |  | ||||||
| nulltemplate = [$hamlet||] |  | ||||||
| 
 |  | ||||||
							
								
								
									
										133
									
								
								hledger-web/Hledger/Web/Settings.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								hledger-web/Hledger/Web/Settings.hs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,133 @@ | |||||||
|  | {-# LANGUAGE CPP #-} | ||||||
|  | module Hledger.Web.Settings | ||||||
|  |     ( hamletFile | ||||||
|  |     , cassiusFile | ||||||
|  |     , juliusFile | ||||||
|  |     , connStr | ||||||
|  |     , ConnectionPool | ||||||
|  |     , withConnectionPool | ||||||
|  |     , runConnectionPool | ||||||
|  |     , approot | ||||||
|  |     , staticroot | ||||||
|  |     , datadir | ||||||
|  |     , staticdir | ||||||
|  |     , templatesdir | ||||||
|  | 
 | ||||||
|  |     , defhost | ||||||
|  |     , defport | ||||||
|  |     , browserstartdelay | ||||||
|  |     , hledgerorgurl | ||||||
|  |     , manualurl | ||||||
|  |     , style_css | ||||||
|  |     , hledger_js | ||||||
|  |     , jquery_js | ||||||
|  |     , jquery_url_js | ||||||
|  |     , dhtmlxcommon_js | ||||||
|  |     , dhtmlxcombo_js | ||||||
|  |     , robots_txt | ||||||
|  |     ) where | ||||||
|  | 
 | ||||||
|  | import System.FilePath ((</>)) | ||||||
|  | import Text.Printf (printf) | ||||||
|  | import qualified Text.Hamlet as H | ||||||
|  | import qualified Text.Cassius as H | ||||||
|  | import qualified Text.Julius as H | ||||||
|  | import Language.Haskell.TH.Syntax | ||||||
|  | import Database.Persist.Sqlite | ||||||
|  | import Yesod (MonadCatchIO) | ||||||
|  | import Yesod.Helpers.Static | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | browserstartdelay = 100000 -- microseconds | ||||||
|  | 
 | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | -- urls | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | 
 | ||||||
|  | hledgerorgurl     = "http://hledger.org" | ||||||
|  | manualurl         = hledgerorgurl++"/MANUAL.html" | ||||||
|  | 
 | ||||||
|  | defhost           = "localhost" | ||||||
|  | defport           = 5000 | ||||||
|  | 
 | ||||||
|  | approot :: String | ||||||
|  | #ifdef PRODUCTION | ||||||
|  | approot = printf "http://%s:%d" defhost (defport :: Int) | ||||||
|  | #else | ||||||
|  | approot = printf "http://%s:%d" defhost (defport :: Int) | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | staticroot :: String | ||||||
|  | staticroot = approot ++ "/static" | ||||||
|  | 
 | ||||||
|  | -- Some static routes we can refer to by name, without hard-coded filesystem location. | ||||||
|  | style_css       = StaticRoute ["style.css"] [] | ||||||
|  | hledger_js      = StaticRoute ["hledger.js"] [] | ||||||
|  | jquery_js       = StaticRoute ["jquery.js"] [] | ||||||
|  | jquery_url_js   = StaticRoute ["jquery.url.js"] [] | ||||||
|  | dhtmlxcommon_js = StaticRoute ["dhtmlxcommon.js"] [] | ||||||
|  | dhtmlxcombo_js  = StaticRoute ["dhtmlxcombo.js"] [] | ||||||
|  | 
 | ||||||
|  | -- Content for /robots.txt | ||||||
|  | robots_txt = "User-agent: *" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | -- filesystem | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | 
 | ||||||
|  | -- XXX hard-coded data directory path. This must be in your current dir | ||||||
|  | -- when you run or compile hledger-web. | ||||||
|  | datadir :: FilePath | ||||||
|  | datadir = "data" | ||||||
|  | 
 | ||||||
|  | staticdir :: FilePath | ||||||
|  | staticdir = datadir ++ "/static" | ||||||
|  | 
 | ||||||
|  | templatesdir :: FilePath | ||||||
|  | templatesdir = datadir ++ "/templates" | ||||||
|  | 
 | ||||||
|  | -- The following are compile-time macros. In production mode, the template | ||||||
|  | -- is read only once at startup, otherwise every time. | ||||||
|  | 
 | ||||||
|  | hamletFile :: FilePath -> Q Exp | ||||||
|  | #ifdef PRODUCTION | ||||||
|  | hamletFile x = H.hamletFile $ templatesdir </> (x ++ ".hamlet") | ||||||
|  | #else | ||||||
|  | hamletFile x = H.hamletFileDebug $ templatesdir </> (x ++ ".hamlet") | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | cassiusFile :: FilePath -> Q Exp | ||||||
|  | #ifdef PRODUCTION | ||||||
|  | cassiusFile x = H.cassiusFile $ templatesdir </> (x ++ ".cassius") | ||||||
|  | #else | ||||||
|  | cassiusFile x = H.cassiusFileDebug $ templatesdir </> (x ++ ".cassius") | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | juliusFile :: FilePath -> Q Exp | ||||||
|  | #ifdef PRODUCTION | ||||||
|  | juliusFile x = H.juliusFile $ templatesdir </> (x ++ ".julius") | ||||||
|  | #else | ||||||
|  | juliusFile x = H.juliusFileDebug $ templatesdir </> (x ++ ".julius") | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | -- database | ||||||
|  | ---------------------------------------------------------------------- | ||||||
|  | 
 | ||||||
|  | connStr :: String | ||||||
|  | #ifdef PRODUCTION | ||||||
|  | connStr = "production.db3" | ||||||
|  | #else | ||||||
|  | connStr = "debug.db3" | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | connectionCount :: Int | ||||||
|  | connectionCount = 10 | ||||||
|  | 
 | ||||||
|  | withConnectionPool :: MonadCatchIO m => (ConnectionPool -> m a) -> m a | ||||||
|  | withConnectionPool = withSqlitePool connStr connectionCount | ||||||
|  | 
 | ||||||
|  | runConnectionPool :: MonadCatchIO m => SqlPersist m a -> ConnectionPool -> m a | ||||||
|  | runConnectionPool = runSqlPool | ||||||
|  | 
 | ||||||
| @ -11,13 +11,27 @@ module Main where | |||||||
| import Prelude hiding (putStr, putStrLn) | import Prelude hiding (putStr, putStrLn) | ||||||
| import System.IO.UTF8 (putStr, putStrLn) | import System.IO.UTF8 (putStr, putStrLn) | ||||||
| #endif | #endif | ||||||
|  | import Control.Concurrent (forkIO, threadDelay) | ||||||
|  | -- import System.FilePath ((</>)) | ||||||
|  | import System.IO.Storage (withStore, putValue,) | ||||||
|  | 
 | ||||||
|  | import Network.Wai.Handler.SimpleServer (run) | ||||||
|  | import Yesod.Content (typeByExt) | ||||||
|  | import Yesod.Helpers.Static (fileLookupDir) | ||||||
| 
 | 
 | ||||||
| import Hledger.Cli.Options | import Hledger.Cli.Options | ||||||
| -- import Hledger.Cli.Tests | -- import Hledger.Cli.Tests | ||||||
| import Hledger.Cli.Utils (withJournalDo) | import Hledger.Cli.Utils (withJournalDo, openBrowserOn) | ||||||
| import Hledger.Cli.Version (versionmsg) --, binaryfilename) | import Hledger.Cli.Version (versionmsg) --, binaryfilename) | ||||||
| import Hledger.Data | import Hledger.Data | ||||||
| import Hledger.Web | import Hledger.Web.App (App(..), withApp) | ||||||
|  | import Hledger.Web.Settings (browserstartdelay, defhost, defport, datadir, staticdir) | ||||||
|  | -- #ifdef MAKE | ||||||
|  | -- import Paths_hledger_web_make (getDataFileName) | ||||||
|  | -- #else | ||||||
|  | -- import Paths_hledger_web (getDataFileName) | ||||||
|  | -- #endif | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| main :: IO () | main :: IO () | ||||||
| main = do | main = do | ||||||
| @ -36,3 +50,36 @@ main = do | |||||||
|        | otherwise                    = putStr help1 |        | otherwise                    = putStr help1 | ||||||
| 
 | 
 | ||||||
|       defaultcmd = Just web |       defaultcmd = Just web | ||||||
|  | 
 | ||||||
|  | -- | The web command. | ||||||
|  | web :: [Opt] -> [String] -> Journal -> IO () | ||||||
|  | web opts args j = do | ||||||
|  |   let host    = defhost | ||||||
|  |       port    = fromMaybe defport $ portFromOpts opts | ||||||
|  |       baseurl = fromMaybe (printf "http://%s:%d" host port) $ baseUrlFromOpts opts | ||||||
|  |   unless (Debug `elem` opts) $ forkIO (browser baseurl) >> return () | ||||||
|  |   server baseurl port opts args j | ||||||
|  | 
 | ||||||
|  | browser :: String -> IO () | ||||||
|  | browser baseurl = do | ||||||
|  |   putStrLn "starting web browser" | ||||||
|  |   threadDelay $ fromIntegral browserstartdelay | ||||||
|  |   openBrowserOn baseurl >> return () | ||||||
|  | 
 | ||||||
|  | server :: String -> Int -> [Opt] -> [String] -> Journal -> IO () | ||||||
|  | server baseurl port opts args j = do | ||||||
|  |   printf "starting web server on port %d with base url %s\n" port baseurl | ||||||
|  |   withStore "hledger" $ do | ||||||
|  |     putValue "hledger" "journal" j | ||||||
|  |     -- dir <- getDataFileName "" | ||||||
|  |     -- let staticdir = dir </> "static" | ||||||
|  |     withApp App{ | ||||||
|  |               appConnPool=Nothing | ||||||
|  |              ,appRoot=baseurl | ||||||
|  |              ,appDataDir=datadir | ||||||
|  |              ,appStatic=fileLookupDir staticdir $ typeByExt -- ++[("hamlet","text/plain")] | ||||||
|  |              ,appOpts=opts | ||||||
|  |              ,appArgs=args | ||||||
|  |              ,appJournal=j | ||||||
|  |              } $ run port | ||||||
|  | 
 | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ input.textinput, .dhx_combo_input, .dhx_combo_list    { font-size:small; } | |||||||
| #filterform                 { font-size:small; } | #filterform                 { font-size:small; } | ||||||
| .topleftlink                { font-size:small; } | .topleftlink                { font-size:small; } | ||||||
| .toprightlink               { font-size:small; } | .toprightlink               { font-size:small; } | ||||||
| #journalinfo                { font-size:small; } | #journaldesc                { font-size:small; } | ||||||
| .help                       { font-size:smaller; } | .help                       { font-size:smaller; } | ||||||
| .form                       { font-size:small; } | .form                       { font-size:small; } | ||||||
| .journalreport              { font-size:small; } | .journalreport              { font-size:small; } | ||||||
|  | |||||||
| @ -7,11 +7,11 @@ description: | |||||||
| license:        GPL | license:        GPL | ||||||
| license-file:   LICENSE | license-file:   LICENSE | ||||||
| author:         Simon Michael <simon@joyful.com> | author:         Simon Michael <simon@joyful.com> | ||||||
| maintainer:      | maintainer:     Simon Michael <simon@joyful.com> | ||||||
| homepage:       http://hledger.org | homepage:       http://hledger.org | ||||||
| bug-reports:    http://code.google.com/p/hledger/issues | bug-reports:    http://code.google.com/p/hledger/issues | ||||||
| stability:      experimental | stability:      alpha | ||||||
| tested-with:    GHC==6.10, GHC==6.12 | tested-with:    GHC==6.12 | ||||||
| cabal-version:  >= 1.6 | cabal-version:  >= 1.6 | ||||||
| build-type:     Simple | build-type:     Simple | ||||||
| data-dir:       data | data-dir:       data | ||||||
| @ -22,7 +22,15 @@ data-files: | |||||||
|                 static/jquery.url.js |                 static/jquery.url.js | ||||||
|                 static/dhtmlxcommon.js |                 static/dhtmlxcommon.js | ||||||
|                 static/dhtmlxcombo.js |                 static/dhtmlxcombo.js | ||||||
|  |                 static/favicon.ico | ||||||
|                 static/images/combo_select.gif |                 static/images/combo_select.gif | ||||||
|  |                 templates/addform.hamlet | ||||||
|  |                 templates/addformpostingfields.hamlet | ||||||
|  |                 templates/default-layout.cassius | ||||||
|  |                 templates/default-layout.hamlet | ||||||
|  |                 templates/homepage.cassius | ||||||
|  |                 templates/homepage.hamlet | ||||||
|  |                 templates/homepage.julius | ||||||
| extra-tmp-files: | extra-tmp-files: | ||||||
| extra-source-files: | extra-source-files: | ||||||
| 
 | 
 | ||||||
| @ -30,11 +38,18 @@ source-repository head | |||||||
|   type:     darcs |   type:     darcs | ||||||
|   location: http://joyful.com/repos/hledger |   location: http://joyful.com/repos/hledger | ||||||
| 
 | 
 | ||||||
|  | Flag production | ||||||
|  |     Description:   Build in production mode, which reads templates only once at startup. | ||||||
|  |     Default:       False | ||||||
|  | 
 | ||||||
| executable hledger-web | executable hledger-web | ||||||
|   main-is:        Main.hs |   main-is:        Main.hs | ||||||
|   ghc-options:    -threaded -W |   ghc-options:    -threaded -W | ||||||
|  |   if flag(production) | ||||||
|  |       cpp-options:   -DPRODUCTION | ||||||
|   other-modules: |   other-modules: | ||||||
|                   Hledger.Web |                   Hledger.Web.App | ||||||
|  |                   Hledger.Web.Settings | ||||||
|   build-depends: |   build-depends: | ||||||
|                   hledger == 0.12.98 |                   hledger == 0.12.98 | ||||||
|                  ,hledger-lib == 0.12.98 |                  ,hledger-lib == 0.12.98 | ||||||
| @ -60,3 +75,7 @@ executable hledger-web | |||||||
|                  ,convertible-text >= 0.3.0.1 && < 0.4 |                  ,convertible-text >= 0.3.0.1 && < 0.4 | ||||||
|                  ,data-object >= 0.3.1.2 && < 0.4 |                  ,data-object >= 0.3.1.2 && < 0.4 | ||||||
|                  ,failure >= 0.1 && < 0.2 |                  ,failure >= 0.1 && < 0.2 | ||||||
|  |                  ,persistent == 0.2.* | ||||||
|  |                  ,persistent-sqlite == 0.2.* | ||||||
|  |                  ,template-haskell == 2.4.* | ||||||
|  |                  ,wai-extra == 0.2.* | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user