hledger-web 1.13 → 1.14
raw patch · 19 files changed
+835/−283 lines, 19 filesdep +Decimaldep +aesondep +containersdep ~hledgerdep ~hledger-libPVP ok
version bump matches the API change (PVP)
Dependencies added: Decimal, aeson, containers, http-types
Dependency ranges changed: hledger, hledger-lib
API changes (from Hackage documentation)
- Hledger.Web.Handler.Common: getDownloadR :: FilePath -> Handler TypedContent
- Hledger.Web.Handler.Common: getFaviconR :: MonadHandler m => m ()
- Hledger.Web.Handler.Common: getManageR :: Handler Html
- Hledger.Web.Handler.Common: getRobotsR :: MonadHandler m => m ()
- Hledger.Web.Handler.Common: getRootR :: Handler Html
- Hledger.Web.Main: withJournalDoWeb :: WebOpts -> (WebOpts -> Journal -> IO a) -> IO a
+ Hledger.Web.Handler.AddR: putAddR :: Handler RepJson
+ Hledger.Web.Handler.MiscR: getAccountnamesR :: Handler TypedContent
+ Hledger.Web.Handler.MiscR: getAccountsR :: Handler TypedContent
+ Hledger.Web.Handler.MiscR: getAccounttransactionsR :: Text -> Handler TypedContent
+ Hledger.Web.Handler.MiscR: getCommoditiesR :: Handler TypedContent
+ Hledger.Web.Handler.MiscR: getDownloadR :: FilePath -> Handler TypedContent
+ Hledger.Web.Handler.MiscR: getFaviconR :: MonadHandler m => m ()
+ Hledger.Web.Handler.MiscR: getManageR :: Handler Html
+ Hledger.Web.Handler.MiscR: getPricesR :: Handler TypedContent
+ Hledger.Web.Handler.MiscR: getRobotsR :: MonadHandler m => m ()
+ Hledger.Web.Handler.MiscR: getRootR :: Handler Html
+ Hledger.Web.Handler.MiscR: getTransactionsR :: Handler TypedContent
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON (Data.Decimal.DecimalRaw GHC.Integer.Type.Integer)
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.Account
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.AccountDeclarationInfo
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.Amount
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.AmountStyle
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.BalanceAssertion
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.DigitGroupStyle
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.GenericSourcePos
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.MarketPrice
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.MixedAmount
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.Posting
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.PostingType
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.Price
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.Side
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.Status
+ Hledger.Web.Json: instance Data.Aeson.Types.FromJSON.FromJSON Hledger.Data.Types.Transaction
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Data.Decimal.Decimal
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.Account
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.Amount
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.AmountStyle
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.BalanceAssertion
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.DigitGroupStyle
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.GenericSourcePos
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.MarketPrice
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.MixedAmount
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.Posting
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.PostingType
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.Price
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.Side
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.Status
+ Hledger.Web.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hledger.Data.Types.Transaction
+ Hledger.Web.Json: instance GHC.Generics.Generic (Data.Decimal.DecimalRaw a)
+ Hledger.Web.Json: readJsonFile :: FromJSON a => FilePath -> IO a
+ Hledger.Web.Json: writeJsonFile :: ToJSON a => FilePath -> a -> IO ()
Files
- CHANGES.md +28/−0
- Hledger/Web/Application.hs +7/−7
- Hledger/Web/Handler/AddR.hs +37/−8
- Hledger/Web/Handler/Common.hs +0/−38
- Hledger/Web/Handler/MiscR.hs +98/−0
- Hledger/Web/Handler/RegisterR.hs +1/−0
- Hledger/Web/Json.hs +160/−0
- Hledger/Web/Main.hs +2/−21
- Hledger/Web/WebOptions.hs +4/−4
- Hledger/Web/Widget/AddForm.hs +84/−45
- config/routes +8/−1
- hledger-web.1 +121/−44
- hledger-web.cabal +14/−9
- hledger-web.info +141/−44
- hledger-web.txt +124/−56
- templates/add-form.hamlet +1/−1
- templates/chart.hamlet +1/−1
- templates/journal.hamlet +2/−2
- templates/register.hamlet +2/−2
CHANGES.md view
@@ -1,6 +1,34 @@ User-visible changes in hledger-web. See also the hledger changelog. +# 1.14 2019-03-01++- serve the same JSON-providing routes as in hledger-api:+ ```+ /accountnames+ /transactions+ /prices+ /commodities+ /accounts+ /accounttransactions/ACCT+ ```+ And allow adding a new transaction by PUT'ing JSON (similar to the+ output of /transactions) to /add. This requires the `add` capability+ (which is enabled by default). Here's how to test with curl:+ ```+ $ curl -s http://127.0.0.1:5000/add -X PUT -H 'Content-Type: application/json' --data-binary @in.json; echo+ ```+ (#316)++- fix unbalanced transaction prevention in the add form++- fix transaction-showing tooltips (#927)++- manual updates: document --capabilities/--capabilities-header and+ editing/uploading/downloading.++- use hledger 1.14+ # 1.13 (2019/02/01) - use hledger 1.13
Hledger/Web/Application.hs view
@@ -15,13 +15,13 @@ import Yesod.Default.Config import Hledger.Data (Journal, nulljournal)-import Hledger.Web.Handler.AddR (getAddR, postAddR)-import Hledger.Web.Handler.Common- (getDownloadR, getFaviconR, getManageR, getRobotsR, getRootR)-import Hledger.Web.Handler.EditR (getEditR, postEditR)-import Hledger.Web.Handler.UploadR (getUploadR, postUploadR)-import Hledger.Web.Handler.JournalR (getJournalR)-import Hledger.Web.Handler.RegisterR (getRegisterR)++import Hledger.Web.Handler.AddR+import Hledger.Web.Handler.MiscR +import Hledger.Web.Handler.EditR+import Hledger.Web.Handler.UploadR+import Hledger.Web.Handler.JournalR+import Hledger.Web.Handler.RegisterR import Hledger.Web.Import import Hledger.Web.WebOptions (WebOpts(serve_))
Hledger/Web/Handler/AddR.hs view
@@ -7,13 +7,21 @@ module Hledger.Web.Handler.AddR ( getAddR , postAddR+ , putAddR ) where +import Data.Aeson.Types (Result(..))+import qualified Data.Text as T+import Network.HTTP.Types.Status (status400)+import Text.Blaze.Html (preEscapedToHtml)+import Yesod+ import Hledger-import Hledger.Cli.Commands.Add (appendToJournalFileOrStdout)+import Hledger.Cli.Commands.Add (appendToJournalFileOrStdout, journalAddTransaction) import Hledger.Web.Import+import Hledger.Web.Json ()+import Hledger.Web.WebOptions (WebOpts(..)) import Hledger.Web.Widget.AddForm (addForm)-import Hledger.Web.Widget.Common (fromFormSuccess) getAddR :: Handler () getAddR = postAddR@@ -24,12 +32,19 @@ when (CapAdd `notElem` caps) (permissionDenied "Missing the 'add' capability") ((res, view), enctype) <- runFormPost $ addForm j today- t <- txnTieKnot <$> fromFormSuccess (showForm view enctype) res- -- XXX(?) move into balanceTransaction- liftIO $ ensureJournalFileExists (journalFilePath j)- liftIO $ appendToJournalFileOrStdout (journalFilePath j) (showTransaction t)- setMessage "Transaction added."- redirect JournalR+ case res of+ FormSuccess res' -> do+ let t = txnTieKnot res'+ -- XXX(?) move into balanceTransaction+ liftIO $ ensureJournalFileExists (journalFilePath j)+ -- XXX why not journalAddTransaction ?+ liftIO $ appendToJournalFileOrStdout (journalFilePath j) (showTransaction t)+ setMessage "Transaction added."+ redirect JournalR+ FormMissing -> showForm view enctype+ FormFailure errs -> do+ mapM_ (setMessage . preEscapedToHtml . T.replace "\n" "<br>") errs+ showForm view enctype where showForm view enctype = sendResponse =<< defaultLayout [whamlet|@@ -38,3 +53,17 @@ <form#addform.form.col-xs-12.col-md-8 method=post enctype=#{enctype}> ^{view} |]++-- Add a single new transaction, send as JSON via PUT, to the journal.+-- The web form handler above should probably use PUT as well. +putAddR :: Handler RepJson+putAddR = do+ VD{caps, j, opts} <- getViewData+ when (CapAdd `notElem` caps) (permissionDenied "Missing the 'add' capability")++ (r :: Result Transaction) <- parseCheckJsonBody+ case r of+ Error err -> sendStatusJSON status400 ("could not parse json: " ++ err ::String)+ Success t -> do+ void $ liftIO $ journalAddTransaction j (cliopts_ opts) t+ sendResponseCreated TransactionsR
− Hledger/Web/Handler/Common.hs
@@ -1,38 +0,0 @@-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}--module Hledger.Web.Handler.Common- ( getDownloadR- , getFaviconR- , getManageR- , getRobotsR- , getRootR- ) where--import qualified Data.Text as T-import Yesod.Default.Handlers (getFaviconR, getRobotsR)--import Hledger (jfiles)-import Hledger.Web.Import-import Hledger.Web.Widget.Common (journalFile404)--getRootR :: Handler Html-getRootR = redirect JournalR--getManageR :: Handler Html-getManageR = do- VD{caps, j} <- getViewData- when (CapManage `notElem` caps) (permissionDenied "Missing the 'manage' capability")- defaultLayout $ do- setTitle "Manage journal"- $(widgetFile "manage")--getDownloadR :: FilePath -> Handler TypedContent-getDownloadR f = do- VD{caps, j} <- getViewData- when (CapManage `notElem` caps) (permissionDenied "Missing the 'manage' capability")- (f', txt) <- journalFile404 f j- addHeader "Content-Disposition" ("attachment; filename=\"" <> T.pack f' <> "\"")- sendResponse ("text/plain" :: ByteString, toContent txt)
+ Hledger/Web/Handler/MiscR.hs view
@@ -0,0 +1,98 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}++{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++module Hledger.Web.Handler.MiscR+ ( getAccountnamesR + , getTransactionsR + , getPricesR + , getCommoditiesR + , getAccountsR + , getAccounttransactionsR+ , getDownloadR+ , getFaviconR+ , getManageR+ , getRobotsR+ , getRootR+ ) where++import qualified Data.Map as M+import qualified Data.Text as T+import Yesod.Default.Handlers (getFaviconR, getRobotsR)++import Hledger+import Hledger.Web.Json ()+import Hledger.Web.Import+import Hledger.Web.Widget.Common (journalFile404)++getRootR :: Handler Html+getRootR = redirect JournalR++getManageR :: Handler Html+getManageR = do+ VD{caps, j} <- getViewData+ when (CapManage `notElem` caps) (permissionDenied "Missing the 'manage' capability")+ defaultLayout $ do+ setTitle "Manage journal"+ $(widgetFile "manage")++getDownloadR :: FilePath -> Handler TypedContent+getDownloadR f = do+ VD{caps, j} <- getViewData+ when (CapManage `notElem` caps) (permissionDenied "Missing the 'manage' capability")+ (f', txt) <- journalFile404 f j+ addHeader "Content-Disposition" ("attachment; filename=\"" <> T.pack f' <> "\"")+ sendResponse ("text/plain" :: ByteString, toContent txt)++-- hledger-web equivalents of hledger-api's handlers++getAccountnamesR :: Handler TypedContent+getAccountnamesR = do+ VD{caps, j} <- getViewData+ when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")+ selectRep $ do+ provideJson $ journalAccountNames j++getTransactionsR :: Handler TypedContent+getTransactionsR = do+ VD{caps, j} <- getViewData+ when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")+ selectRep $ do+ provideJson $ jtxns j++getPricesR :: Handler TypedContent+getPricesR = do+ VD{caps, j} <- getViewData+ when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")+ selectRep $ do+ provideJson $ jmarketprices j++getCommoditiesR :: Handler TypedContent+getCommoditiesR = do+ VD{caps, j} <- getViewData+ when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")+ selectRep $ do+ provideJson $ (M.keys . jinferredcommodities) j++getAccountsR :: Handler TypedContent+getAccountsR = do+ VD{caps, j} <- getViewData+ when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")+ selectRep $ do+ provideJson $ ledgerTopAccounts $ ledgerFromJournal Any j++getAccounttransactionsR :: Text -> Handler TypedContent+getAccounttransactionsR a = do+ VD{caps, j} <- getViewData+ when (CapView `notElem` caps) (permissionDenied "Missing the 'view' capability")+ let+ ropts = defreportopts+ q = Any --filterQuery (not . queryIsDepth) $ queryFromOpts d ropts'+ thisacctq = Acct $ accountNameToAccountRegex a -- includes subs+ selectRep $ do+ provideJson $ accountTransactionsReport ropts j q thisacctq+
Hledger/Web/Handler/RegisterR.hs view
@@ -58,3 +58,4 @@ read (formatTime defaultTimeLocale "%s" t) * 1000 -- XXX read where t = UTCTime d (secondsToDiffTime 0)+
+ Hledger/Web/Json.hs view
@@ -0,0 +1,160 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}++--{-# LANGUAGE CPP #-}+--{-# LANGUAGE DataKinds #-}+--{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+--{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+--{-# LANGUAGE NamedFieldPuns #-}+--{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings #-}+--{-# LANGUAGE PolyKinds #-}+--{-# LANGUAGE QuasiQuotes #-}+--{-# LANGUAGE QuasiQuotes #-}+--{-# LANGUAGE Rank2Types #-}+--{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+--{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+--{-# LANGUAGE TemplateHaskell #-}+--{-# LANGUAGE TypeFamilies #-}+--{-# LANGUAGE TypeOperators #-}++module Hledger.Web.Json ( + -- * Instances+ -- * Utilities+ readJsonFile+ ,writeJsonFile+) where++import Data.Aeson+--import Data.Aeson.TH+import qualified Data.ByteString.Lazy as BL+import Data.Decimal+import Data.Maybe+import GHC.Generics (Generic)++import Hledger.Data++-- JSON instances. See also hledger-api.+-- Should they be in hledger-lib Types.hs ?++-- To JSON++instance ToJSON Status+instance ToJSON GenericSourcePos+instance ToJSON Decimal+instance ToJSON Amount+instance ToJSON AmountStyle+instance ToJSON Side+instance ToJSON DigitGroupStyle+instance ToJSON MixedAmount+instance ToJSON BalanceAssertion+instance ToJSON Price+instance ToJSON MarketPrice+instance ToJSON PostingType+instance ToJSON Posting where+ toJSON Posting{..} = object+ ["pdate" .= toJSON pdate+ ,"pdate2" .= toJSON pdate2+ ,"pstatus" .= toJSON pstatus+ ,"paccount" .= toJSON paccount+ ,"pamount" .= toJSON pamount+ ,"pcomment" .= toJSON pcomment+ ,"ptype" .= toJSON ptype+ ,"ptags" .= toJSON ptags+ ,"pbalanceassertion" .= toJSON pbalanceassertion+ -- To avoid a cycle, show just the parent transaction's index number + -- in a dummy field. When re-parsed, there will be no parent.+ ,"ptransaction_" .= toJSON (maybe "" (show.tindex) ptransaction)+ -- This is probably not wanted in json, we discard it.+ ,"poriginal" .= toJSON (Nothing :: Maybe Posting) + ]+instance ToJSON Transaction+instance ToJSON Account where+ toJSON a = object+ ["aname" .= toJSON (aname a)+ ,"aebalance" .= toJSON (aebalance a)+ ,"aibalance" .= toJSON (aibalance a)+ ,"anumpostings" .= toJSON (anumpostings a)+ ,"aboring" .= toJSON (aboring a)+ -- To avoid a cycle, show just the parent account's name + -- in a dummy field. When re-parsed, there will be no parent.+ ,"aparent_" .= toJSON (maybe "" aname $ aparent a)+ -- To avoid a cycle, drop the subaccounts, showing just their names+ -- in a dummy field. When re-parsed, there will be no subaccounts.+ ,"asubs" .= toJSON ([]::[Account])+ ,"asubs_" .= toJSON (map aname $ asubs a)+ ]++-- From JSON++instance FromJSON Status+instance FromJSON GenericSourcePos+instance FromJSON Amount+instance FromJSON AmountStyle+instance FromJSON Side+instance FromJSON DigitGroupStyle+instance FromJSON MixedAmount+instance FromJSON BalanceAssertion+instance FromJSON Price+instance FromJSON MarketPrice+instance FromJSON PostingType+instance FromJSON Posting+instance FromJSON Transaction+instance FromJSON AccountDeclarationInfo+instance FromJSON Account++-- Decimal, various attempts+--+-- https://stackoverflow.com/questions/40331851/haskell-data-decimal-as-aeson-type+----instance FromJSON Decimal where parseJSON = +---- A.withScientific "Decimal" (return . right . eitherFromRational . toRational)+--+-- https://github.com/bos/aeson/issues/474+-- http://hackage.haskell.org/package/aeson-1.4.2.0/docs/Data-Aeson-TH.html+-- $(deriveFromJSON defaultOptions ''Decimal) -- doesn't work+-- $(deriveFromJSON defaultOptions ''DecimalRaw) -- works; requires TH, but gives better parse error messages+--+-- https://github.com/PaulJohnson/Haskell-Decimal/issues/6+--deriving instance Generic Decimal+--instance FromJSON Decimal+deriving instance Generic (DecimalRaw a)+instance FromJSON (DecimalRaw Integer)+--+-- @simonmichael, I think the code in your first comment should work if it compiles—though “work” doesn’t mean you can parse a JSON number directly into a `Decimal` using the generic instance, as you’ve discovered.+--+--Error messages with these extensions are always rather cryptic, but I’d prefer them to Template Haskell. Typically you’ll want to start by getting a generic `ToJSON` instance working, then use that to figure out what the `FromJSON` instance expects to parse: for a correct instance, `encode` and `decode` should give you an isomorphism between your type and a subset of `Bytestring` (up to the `Maybe` wrapper that `decode` returns).+--+--I don’t have time to test it right now, but I think it will also work without `DeriveAnyClass`, just using `DeriveGeneric` and `StandAloneDeriving`. It should also work to use the [`genericParseJSON`](http://hackage.haskell.org/package/aeson/docs/Data-Aeson.html#v:genericParseJSON) function to implement the class explicitly, something like this:+--+--{-# LANGUAGE DeriveGeneric #-}+--{-# LANGUAGE StandAloneDeriving #-}+--import GHC.Generics+--import Data.Aeson+--deriving instance Generic Decimal+--instance FromJSON Decimal where+-- parseJSON = genericParseJSON defaultOptions+--+--And of course you can avoid `StandAloneDeriving` entirely if you’re willing to wrap `Decimal` in your own `newtype`.+++-- Utilities++-- | Read a json from a file and decode/parse it as the target type, if we can.+-- Example:+-- >>> readJsonFile "in.json" :: IO MixedAmount+readJsonFile :: FromJSON a => FilePath -> IO a+readJsonFile f = do+ bs <- BL.readFile f+ let v = fromMaybe (error "could not decode bytestring as json value") (decode bs :: Maybe Value) + case fromJSON v :: FromJSON a => Result a of+ Error e -> error e+ Success t -> return t++-- | Write some to-JSON-convertible haskell value to a json file, if we can.+-- Example:+-- >>> writeJsonFile "out.json" nullmixedamt+writeJsonFile :: ToJSON a => FilePath -> a -> IO ()+writeJsonFile f v = BL.writeFile f (encode $ toJSON v)
Hledger/Web/Main.hs view
@@ -38,7 +38,7 @@ hledgerWebDev :: IO (Int, Application) hledgerWebDev =- withJournalDoWeb defwebopts (\o j -> defaultDevelApp loader $ makeApplication o j)+ withJournalDo (cliopts_ defwebopts) (defaultDevelApp loader . makeApplication defwebopts) where loader = Yesod.Default.Config.loadConfig@@ -49,26 +49,7 @@ | "help" `inRawOpts` rawopts_ (cliopts_ opts) = putStr (showModeUsage webmode) >> exitSuccess | "version" `inRawOpts` rawopts_ (cliopts_ opts) = putStrLn prognameandversion >> exitSuccess | "binary-filename" `inRawOpts` rawopts_ (cliopts_ opts) = putStrLn (binaryfilename progname)- | otherwise = withJournalDoWeb opts web---- | A version of withJournalDo specialised for hledger-web.--- Disallows the special - file to avoid some bug,--- takes WebOpts rather than CliOpts.-withJournalDoWeb :: WebOpts -> (WebOpts -> Journal -> IO a) -> IO a-withJournalDoWeb opts@WebOpts {cliopts_ = copts} cmd = do- journalpaths <- journalFilePathFromOpts copts-- -- https://github.com/simonmichael/hledger/issues/202- -- -f- gives [Error#yesod-core] <stdin>: hGetContents: illegal operation (handle is closed)- -- Also we may try to write to this file. Just disallow -.- when ("-" `elem` journalpaths) $ -- always non-empty- error' "hledger-web doesn't support -f -, please specify a file path"- mapM_ requireJournalFileExists journalpaths-- -- keep synced with withJournalDo TODO refactor- readJournalFiles (inputopts_ copts) journalpaths- >>= mapM (journalTransform copts)- >>= either error' (cmd opts)+ | otherwise = withJournalDo (cliopts_ opts) (web opts) -- | The web command. web :: WebOpts -> Journal -> IO ()
Hledger/Web/WebOptions.hs view
@@ -54,13 +54,13 @@ , flagReq ["capabilities"] (\s opts -> Right $ setopt "capabilities" s opts)- "CAP,CAP2"- "enable these capabilities - comma-separated, possible values are: view, add, manage (default: view,add)"+ "CAP[,CAP..]"+ "enable the view, add, and/or manage capabilities (default: view,add)" , flagReq ["capabilities-header"] (\s opts -> Right $ setopt "capabilities-header" s opts)- "HEADER"- "read enabled capabilities from a HTTP header (e.g. X-Sandstorm-Permissions, disabled by default)"+ "HTTPHEADER"+ "read capabilities to enable from a HTTP header, like X-Sandstorm-Permissions (default: disabled)" ] webmode :: Mode [(String, String)]
Hledger/Web/Widget/AddForm.hs view
@@ -21,7 +21,7 @@ import Data.Time (Day) import Text.Blaze.Internal (Markup, preEscapedString) import Text.JSON-import Text.Megaparsec (eof, errorBundlePretty, runParser)+import Text.Megaparsec (bundleErrors, eof, parseErrorTextPretty, runParser) import Yesod import Hledger@@ -67,13 +67,7 @@ (descRes, descView) <- mreq textField descFS Nothing (acctRes, _) <- mreq listField acctFS Nothing (amtRes, _) <- mreq listField amtFS Nothing-- let (msgs', postRes) = case validatePostings <$> acctRes <*> amtRes of- FormSuccess (Left es) -> (es, FormFailure ["Postings validation failed"])- FormSuccess (Right xs) -> ([], FormSuccess xs)- FormMissing -> ([], FormMissing)- FormFailure es -> ([], FormFailure es)- msgs = zip [(1 :: Int)..] $ msgs' ++ replicate (4 - length msgs') ("", "", Nothing, Nothing)+ let (postRes, displayRows) = validatePostings acctRes amtRes let descriptions = sort $ nub $ tdescription <$> jtxns j escapeJSSpecialChars = regexReplaceCI "</script>" "<\\/script>" -- #236@@ -81,11 +75,8 @@ encode . JSArray . fmap (\a -> JSObject $ toJSObject [("value", showJSON a)]) journals = fst <$> jfiles j - pure (makeTransaction <$> dateRes <*> descRes <*> postRes, $(widgetFile "add-form"))+ pure (validateTransaction dateRes descRes postRes, $(widgetFile "add-form")) where- makeTransaction date desc postings =- nulltransaction {tdate = date, tdescription = desc, tpostings = postings}- dateFS = FieldSettings "date" Nothing Nothing (Just "date") [("class", "form-control input-lg"), ("placeholder", "Date")] descFS = FieldSettings "desc" Nothing Nothing (Just "description")@@ -103,42 +94,90 @@ , fieldEnctype = UrlEncoded } -validatePostings :: [Text] -> [Text] -> Either [(Text, Text, Maybe Text, Maybe Text)] [Posting]-validatePostings a b =- case traverse id $ (\(_, _, x) -> x) <$> postings of- Left _ -> Left $ foldr catPostings [] postings- Right [] -> Left- [ ("", "", Just "Missing account", Just "Missing amount")- , ("", "", Just "Missing account", Nothing)- ]- Right [p] -> Left- [ (paccount p, T.pack . showMixedAmountWithoutPrice $ pamount p, Nothing, Nothing)- , ("", "", Just "Missing account", Nothing)- ]- Right xs -> Right xs+validateTransaction ::+ FormResult Day+ -> FormResult Text+ -> FormResult [Posting]+ -> FormResult Transaction+validateTransaction dateRes descRes postingsRes =+ case makeTransaction <$> dateRes <*> descRes <*> postingsRes of+ FormSuccess txn -> case balanceTransaction Nothing txn of+ Left e -> FormFailure [T.pack e]+ Right txn' -> FormSuccess txn'+ x -> x where- postings = unfoldr go (True, a, b)+ makeTransaction date desc postings =+ nulltransaction {tdate = date, tdescription = desc, tpostings = postings} - go (_, x:xs, y:ys) = Just ((x, y, zipPosting (validateAccount x) (validateAmount y)), (True, xs, ys))- go (True, x:y:xs, []) = Just ((x, "", zipPosting (validateAccount x) (Left "Missing amount")), (True, y:xs, []))- go (True, x:xs, []) = Just ((x, "", zipPosting (validateAccount x) (Right missingamt)), (False, xs, []))- go (False, x:xs, []) = Just ((x, "", zipPosting (validateAccount x) (Left "Missing amount")), (False, xs, []))- go (_, [], y:ys) = Just (("", y, zipPosting (Left "Missing account") (validateAmount y)), (False, [], ys))- go (_, [], []) = Nothing - zipPosting = zipEither (\acc amt -> nullposting {paccount = acc, pamount = Mixed [amt]})+-- | Parse a list of postings out of a list of accounts and a corresponding list+-- of amounts+validatePostings ::+ FormResult [Text]+ -> FormResult [Text]+ -> (FormResult [Posting], [(Int, (Text, Text, Maybe Text, Maybe Text))])+validatePostings acctRes amtRes = let - catPostings (t, t', Left (e, e')) xs = (t, t', e, e') : xs- catPostings (t, t', Right _) xs = (t, t', Nothing, Nothing) : xs+ -- Zip accounts and amounts, fill in missing values and drop empty rows.+ rows :: [(Text, Text)]+ rows = filter (/= ("", "")) $ zipDefault "" (formSuccess [] acctRes) (formSuccess [] amtRes) - errorToFormMsg = first (("Invalid value: " <>) . T.pack . errorBundlePretty)- validateAccount = errorToFormMsg . runParser (accountnamep <* eof) "" . T.strip- validateAmount = errorToFormMsg . runParser (evalStateT (amountp <* eof) mempty) "" . T.strip+ -- Parse values and check for incomplete rows with only an account or an amount.+ -- The boolean in unfoldr state is for special handling of 'missingamt', where+ -- one row may have only an account and not an amount.+ postings :: [(Text, Text, Either (Maybe Text, Maybe Text) Posting)]+ postings = unfoldr go (True, rows)+ go (True, (x, ""):y:xs) = Just ((x, "", zipRow (checkAccount x) (Left "Missing amount")), (True, y:xs))+ go (True, (x, ""):xs) = Just ((x, "", zipRow (checkAccount x) (Right missingamt)), (False, xs))+ go (False, (x, ""):xs) = Just ((x, "", zipRow (checkAccount x) (Left "Missing amount")), (False, xs))+ go (_, ("", y):xs) = Just (("", y, zipRow (Left "Missing account") (checkAmount y)), (False, xs))+ go (_, (x, y):xs) = Just ((x, y, zipRow (checkAccount x) (checkAmount y)), (True, xs))+ go (_, []) = Nothing --- Modification of Align, from the `these` package-zipEither :: (a -> a' -> r) -> Either e a -> Either e' a' -> Either (Maybe e, Maybe e') r-zipEither f a b = case (a, b) of- (Right a', Right b') -> Right (f a' b')- (Left a', Right _) -> Left (Just a', Nothing)- (Right _, Left b') -> Left (Nothing, Just b')- (Left a', Left b') -> Left (Just a', Just b')+ zipRow (Left e) (Left e') = Left (Just e, Just e')+ zipRow (Left e) (Right _) = Left (Just e, Nothing)+ zipRow (Right _) (Left e) = Left (Nothing, Just e)+ zipRow (Right acct) (Right amt) = Right (nullposting {paccount = acct, pamount = Mixed [amt]})++ errorToFormMsg = first (("Invalid value: " <>) . T.pack .+ foldl (\s a -> s <> parseErrorTextPretty a) "" .+ bundleErrors)+ checkAccount = errorToFormMsg . runParser (accountnamep <* eof) "" . T.strip+ checkAmount = errorToFormMsg . runParser (evalStateT (amountp <* eof) mempty) "" . T.strip++ -- Add errors to forms with zero or one rows if the form is not a FormMissing+ result :: [(Text, Text, Either (Maybe Text, Maybe Text) Posting)]+ result = case (acctRes, amtRes) of+ (FormMissing, FormMissing) -> postings+ _ -> case postings of+ [] -> [ ("", "", Left (Just "Missing account", Just "Missing amount"))+ , ("", "", Left (Just "Missing account", Nothing))+ ]+ [x] -> [x, ("", "", Left (Just "Missing account", Nothing))]+ xs -> xs++ -- Prepare rows for rendering - resolve Eithers into error messages and pad to+ -- at least four rows+ display' = flip fmap result $ \(acc, amt, res) -> case res of+ Left (mAccountErr, mAmountErr) -> (acc, amt, mAccountErr, mAmountErr)+ Right _ -> (acc, amt, Nothing, Nothing)+ display = display' ++ replicate (4 - length display') ("", "", Nothing, Nothing)++ -- And finally prepare the final FormResult [Posting]+ formResult = case traverse (\(_, _, x) -> x) result of+ Left _ -> FormFailure ["Postings validation failed"]+ Right xs -> FormSuccess xs++ in (formResult, zip [(1 :: Int)..] display)+++zipDefault :: a -> [a] -> [a] -> [(a, a)]+zipDefault def (b:bs) (c:cs) = (b, c):(zipDefault def bs cs)+zipDefault def (b:bs) [] = (b, def):(zipDefault def bs [])+zipDefault def [] (c:cs) = (def, c):(zipDefault def [] cs)+zipDefault _ _ _ = []++formSuccess :: a -> FormResult a -> a+formSuccess def res = case res of+ FormSuccess x -> x+ _ -> def
config/routes view
@@ -5,9 +5,16 @@ / RootR GET /journal JournalR GET /register RegisterR GET-/add AddR GET POST+/add AddR GET POST PUT /manage ManageR GET /edit/#FilePath EditR GET POST /upload/#FilePath UploadR GET POST /download/#FilePath DownloadR GET++/accountnames AccountnamesR GET+/transactions TransactionsR GET+/prices PricesR GET+/commodities CommoditiesR GET+/accounts AccountsR GET+/accounttransactions/#AccountName AccounttransactionsR GET
hledger-web.1 view
@@ -1,5 +1,5 @@ -.TH "hledger\-web" "1" "February 2019" "hledger\-web 1.13" "hledger User Manuals"+.TH "hledger\-web" "1" "March 2019" "hledger\-web 1.14" "hledger User Manuals" @@ -41,54 +41,15 @@ \f[C]$LEDGER_FILE\f[], or \f[C]$HOME/.hledger.journal\f[] (on windows, perhaps \f[C]C:/Users/USER/.hledger.journal\f[]). For more about this see hledger(1), hledger_journal(5) etc.-.PP-By default, hledger\-web starts the web app in "transient mode" and also-opens it in your default web browser if possible.-In this mode the web app will keep running for as long as you have it-open in a browser window, and will exit after two minutes of inactivity-(no requests and no browser windows viewing it).-With \f[C]\-\-serve\f[], it just runs the web app without exiting, and-logs requests to the console.-.PP-By default the server listens on IP address 127.0.0.1, accessible only-to local requests.-You can use \f[C]\-\-host\f[] to change this, eg-\f[C]\-\-host\ 0.0.0.0\f[] to listen on all configured addresses.-.PP-Similarly, use \f[C]\-\-port\f[] to set a TCP port other than 5000, eg-if you are running multiple hledger\-web instances.-.PP-You can use \f[C]\-\-base\-url\f[] to change the protocol, hostname,-port and path that appear in hyperlinks, useful eg for integrating-hledger\-web within a larger website.-The default is \f[C]http://HOST:PORT/\f[] using the server\[aq]s-configured host address and TCP port (or \f[C]http://HOST\f[] if PORT is-80).-.PP-With \f[C]\-\-file\-url\f[] you can set a different base url for static-files, eg for better caching or cookie\-less serving on high performance-websites.-.PP-Note there is no built\-in access control (aside from listening on-127.0.0.1 by default).-So you will need to hide hledger\-web behind an authenticating proxy-(such as apache or nginx) if you want to restrict who can see and add-entries to your journal.+.SH OPTIONS .PP Command\-line options and arguments may be used to set an initial filter on the data.-This is not shown in the web UI, but it will be applied in addition to-any search query entered there.-.PP-With journal and timeclock files (but not CSV files, currently) the web-app detects changes made by other means and will show the new data on-the next request.-If a change makes the file unparseable, hledger\-web will show an error-until the file has been fixed.-.SH OPTIONS+These filter options are not shown in the web UI, but it will be applied+in addition to any search query entered there. .PP Note: if invoking hledger\-web as a hledger subcommand, write-\f[C]\-\-\f[] before options as shown above.+\f[C]\-\-\f[] before options, as shown in the synopsis above. .TP .B \f[C]\-\-serve\f[] serve and log requests, don\[aq]t browse or auto\-exit@@ -119,6 +80,17 @@ with this. .RS .RE+.TP+.B \f[C]\-\-capabilities=CAP[,CAP..]\f[]+enable the view, add, and/or manage capabilities (default: view,add)+.RS+.RE+.TP+.B \f[C]\-\-capabilities\-header=HTTPHEADER\f[]+read capabilities to enable from a HTTP header, like+X\-Sandstorm\-Permissions (default: disabled)+.RS+.RE .PP hledger input options: .TP@@ -286,6 +258,111 @@ A \@FILE argument will be expanded to the contents of FILE, which should contain one command line option/argument per line. (To prevent this, insert a \f[C]\-\-\f[] argument before.)+.PP+By default, hledger\-web starts the web app in "transient mode" and also+opens it in your default web browser if possible.+In this mode the web app will keep running for as long as you have it+open in a browser window, and will exit after two minutes of inactivity+(no requests and no browser windows viewing it).+With \f[C]\-\-serve\f[], it just runs the web app without exiting, and+logs requests to the console.+.PP+By default the server listens on IP address 127.0.0.1, accessible only+to local requests.+You can use \f[C]\-\-host\f[] to change this, eg+\f[C]\-\-host\ 0.0.0.0\f[] to listen on all configured addresses.+.PP+Similarly, use \f[C]\-\-port\f[] to set a TCP port other than 5000, eg+if you are running multiple hledger\-web instances.+.PP+You can use \f[C]\-\-base\-url\f[] to change the protocol, hostname,+port and path that appear in hyperlinks, useful eg for integrating+hledger\-web within a larger website.+The default is \f[C]http://HOST:PORT/\f[] using the server\[aq]s+configured host address and TCP port (or \f[C]http://HOST\f[] if PORT is+80).+.PP+With \f[C]\-\-file\-url\f[] you can set a different base url for static+files, eg for better caching or cookie\-less serving on high performance+websites.+.SH PERMISSIONS+.PP+By default, hledger\-web allows anyone who can reach it to view the+journal and to add new transactions, but not to change existing data.+.PP+You can restrict who can reach it by+.IP \[bu] 2+setting the IP address it listens on (see \f[C]\-\-host\f[] above).+By default it listens on 127.0.0.1, accessible to all users on the local+machine.+.IP \[bu] 2+putting it behind an authenticating proxy, using eg apache or nginx+.IP \[bu] 2+custom firewall rules+.PP+You can restrict what the users who reach it can do, by+.IP \[bu] 2+using the \f[C]\-\-capabilities=CAP[,CAP..]\f[] flag when you start it,+enabling one or more of the following capabilities.+The default value is \f[C]view,add\f[]:+.RS 2+.IP \[bu] 2+\f[C]view\f[] \- allows viewing the journal file and all included files+.IP \[bu] 2+\f[C]add\f[] \- allows adding new transactions to the main journal file+.IP \[bu] 2+\f[C]manage\f[] \- allows editing, uploading or downloading the main or+included files+.RE+.IP \[bu] 2+using the \f[C]\-\-capabilities\-header=HTTPHEADER\f[] flag to specify a+HTTP header from which it will read capabilities to enable.+hledger\-web on Sandstorm uses the X\-Sandstorm\-Permissions header to+integrate with Sandstorm\[aq]s permissions.+This is disabled by default.+.SH EDITING, UPLOADING, DOWNLOADING+.PP+If you enable the \f[C]manage\f[] capability mentioned above, you\[aq]ll+see a new "spanner" button to the right of the search form.+Clicking this will let you edit, upload, or download the journal file or+any files it includes.+.PP+Note, unlike any other hledger command, in this mode you (or any+visitor) can alter or wipe the data files.+.PP+Normally whenever a file is changed in this way, hledger\-web saves a+numbered backup (assuming file permissions allow it, the disk is not+full, etc.) hledger\-web is not aware of version control systems,+currently; if you use one, you\[aq]ll have to arrange to commit the+changes yourself (eg with a cron job or a file watcher like entr).+.PP+Changes which would leave the journal file(s) unparseable or non\-valid+(eg with failing balance assertions) are prevented.+(Probably.+This needs re\-testing.)+.SH RELOADING+.PP+hledger\-web detects changes made to the files by other means (eg if you+edit it directly, outside of hledger\-web), and it will show the new+data when you reload the page or navigate to a new page.+If a change makes a file unparseable, hledger\-web will display an error+message until the file has been fixed.+.SH JSON API+.PP+In addition to the web UI, hledger\-web provides some JSON API routes.+These are similar to the API provided by the hledger\-api tool, but it+may be convenient to have them in hledger\-web also.+.IP+.nf+\f[C]+/accountnames+/transactions+/prices+/commodities+/accounts+/accounttransactions/#AccountName+\f[]+.fi .SH ENVIRONMENT .PP \f[B]LEDGER_FILE\f[] The journal file path when not specified with
hledger-web.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: b8f9535cea480d164624211cb8b2a0e0cfc7876b3c2c38539c62fe21ef588d14+-- hash: da546060d989bf42b59988f7db214e93c1ac3e3d166272cbcdbf1c8cbd283187 name: hledger-web-version: 1.13+version: 1.14 synopsis: Web interface for the hledger accounting tool description: This is hledger's web interface. It provides a more user-friendly and collaborative UI than the@@ -27,7 +27,7 @@ maintainer: Simon Michael <simon@joyful.com> license: GPL-3 license-file: LICENSE-tested-with: GHC==7.10.3, GHC==8.0.2, GHC==8.2.2, GHC==8.4.3+tested-with: GHC==7.10.3, GHC==8.0.2, GHC==8.2.2, GHC==8.4.3, GHC==8.6.3 build-type: Simple extra-source-files: CHANGES.md@@ -133,12 +133,13 @@ Hledger.Web.Application Hledger.Web.Foundation Hledger.Web.Handler.AddR- Hledger.Web.Handler.Common Hledger.Web.Handler.EditR Hledger.Web.Handler.JournalR+ Hledger.Web.Handler.MiscR Hledger.Web.Handler.RegisterR Hledger.Web.Handler.UploadR Hledger.Web.Import+ Hledger.Web.Json Hledger.Web.Main Hledger.Web.Settings Hledger.Web.Settings.StaticFiles@@ -150,9 +151,11 @@ hs-source-dirs: ./. ghc-options: -Wall -fwarn-tabs- cpp-options: -DVERSION="1.13"+ cpp-options: -DVERSION="1.14" build-depends:- base >=4.8 && <4.13+ Decimal+ , aeson+ , base >=4.8 && <4.13 , blaze-html , blaze-markup , bytestring@@ -161,14 +164,16 @@ , cmdargs >=0.10 , conduit , conduit-extra >=1.1+ , containers , data-default , directory , filepath , hjsmin- , hledger >=1.13 && <1.14- , hledger-lib >=1.13 && <1.14+ , hledger >=1.14 && <1.15+ , hledger-lib >=1.14 && <1.15 , http-client , http-conduit+ , http-types , json , megaparsec >=7.0.0 && <8 , mtl@@ -206,7 +211,7 @@ hs-source-dirs: app ghc-options: -Wall -fwarn-tabs- cpp-options: -DVERSION="1.13"+ cpp-options: -DVERSION="1.14" build-depends: base , hledger-web
hledger-web.info view
@@ -3,7 +3,7 @@ File: hledger-web.info, Node: Top, Next: OPTIONS, Up: (dir) -hledger-web(1) hledger-web 1.13+hledger-web(1) hledger-web 1.14 ******************************* hledger-web is hledger's web interface. It starts a simple web@@ -25,57 +25,27 @@ '$LEDGER_FILE', or '$HOME/.hledger.journal' (on windows, perhaps 'C:/Users/USER/.hledger.journal'). For more about this see hledger(1), hledger_journal(5) etc.-- By default, hledger-web starts the web app in "transient mode" and-also opens it in your default web browser if possible. In this mode the-web app will keep running for as long as you have it open in a browser-window, and will exit after two minutes of inactivity (no requests and-no browser windows viewing it). With '--serve', it just runs the web-app without exiting, and logs requests to the console.-- By default the server listens on IP address 127.0.0.1, accessible-only to local requests. You can use '--host' to change this, eg '--host-0.0.0.0' to listen on all configured addresses.-- Similarly, use '--port' to set a TCP port other than 5000, eg if you-are running multiple hledger-web instances.-- You can use '--base-url' to change the protocol, hostname, port and-path that appear in hyperlinks, useful eg for integrating hledger-web-within a larger website. The default is 'http://HOST:PORT/' using the-server's configured host address and TCP port (or 'http://HOST' if PORT-is 80).-- With '--file-url' you can set a different base url for static files,-eg for better caching or cookie-less serving on high performance-websites.-- Note there is no built-in access control (aside from listening on-127.0.0.1 by default). So you will need to hide hledger-web behind an-authenticating proxy (such as apache or nginx) if you want to restrict-who can see and add entries to your journal.-- Command-line options and arguments may be used to set an initial-filter on the data. This is not shown in the web UI, but it will be-applied in addition to any search query entered there.-- With journal and timeclock files (but not CSV files, currently) the-web app detects changes made by other means and will show the new data-on the next request. If a change makes the file unparseable,-hledger-web will show an error until the file has been fixed. * Menu: * OPTIONS::+* PERMISSIONS::+* EDITING UPLOADING DOWNLOADING::+* RELOADING::+* JSON API:: -File: hledger-web.info, Node: OPTIONS, Prev: Top, Up: Top+File: hledger-web.info, Node: OPTIONS, Next: PERMISSIONS, Prev: Top, Up: Top 1 OPTIONS ********* -Note: if invoking hledger-web as a hledger subcommand, write '--' before-options as shown above.+Command-line options and arguments may be used to set an initial filter+on the data. These filter options are not shown in the web UI, but it+will be applied in addition to any search query entered there. + Note: if invoking hledger-web as a hledger subcommand, write '--'+before options, as shown in the synopsis above.+ '--serve' serve and log requests, don't browse or auto-exit@@ -96,7 +66,15 @@ normally serves static files itself, but if you wanted to serve them from another server for efficiency, you would set the url with this.+'--capabilities=CAP[,CAP..]' + enable the view, add, and/or manage capabilities (default:+ view,add)+'--capabilities-header=HTTPHEADER'++ read capabilities to enable from a HTTP header, like+ X-Sandstorm-Permissions (default: disabled)+ hledger input options: '-f FILE --file=FILE'@@ -209,10 +187,129 @@ should contain one command line option/argument per line. (To prevent this, insert a '--' argument before.) + By default, hledger-web starts the web app in "transient mode" and+also opens it in your default web browser if possible. In this mode the+web app will keep running for as long as you have it open in a browser+window, and will exit after two minutes of inactivity (no requests and+no browser windows viewing it). With '--serve', it just runs the web+app without exiting, and logs requests to the console.++ By default the server listens on IP address 127.0.0.1, accessible+only to local requests. You can use '--host' to change this, eg '--host+0.0.0.0' to listen on all configured addresses.++ Similarly, use '--port' to set a TCP port other than 5000, eg if you+are running multiple hledger-web instances.++ You can use '--base-url' to change the protocol, hostname, port and+path that appear in hyperlinks, useful eg for integrating hledger-web+within a larger website. The default is 'http://HOST:PORT/' using the+server's configured host address and TCP port (or 'http://HOST' if PORT+is 80).++ With '--file-url' you can set a different base url for static files,+eg for better caching or cookie-less serving on high performance+websites.+ +File: hledger-web.info, Node: PERMISSIONS, Next: EDITING UPLOADING DOWNLOADING, Prev: OPTIONS, Up: Top++2 PERMISSIONS+*************++By default, hledger-web allows anyone who can reach it to view the+journal and to add new transactions, but not to change existing data.++ You can restrict who can reach it by++ * setting the IP address it listens on (see '--host' above). By+ default it listens on 127.0.0.1, accessible to all users on the+ local machine.+ * putting it behind an authenticating proxy, using eg apache or nginx+ * custom firewall rules++ You can restrict what the users who reach it can do, by++ * using the '--capabilities=CAP[,CAP..]' flag when you start it,+ enabling one or more of the following capabilities. The default+ value is 'view,add':+ * 'view' - allows viewing the journal file and all included+ files+ * 'add' - allows adding new transactions to the main journal+ file+ * 'manage' - allows editing, uploading or downloading the main+ or included files++ * using the '--capabilities-header=HTTPHEADER' flag to specify a HTTP+ header from which it will read capabilities to enable. hledger-web+ on Sandstorm uses the X-Sandstorm-Permissions header to integrate+ with Sandstorm's permissions. This is disabled by default.+++File: hledger-web.info, Node: EDITING UPLOADING DOWNLOADING, Next: RELOADING, Prev: PERMISSIONS, Up: Top++3 EDITING, UPLOADING, DOWNLOADING+*********************************++If you enable the 'manage' capability mentioned above, you'll see a new+"spanner" button to the right of the search form. Clicking this will+let you edit, upload, or download the journal file or any files it+includes.++ Note, unlike any other hledger command, in this mode you (or any+visitor) can alter or wipe the data files.++ Normally whenever a file is changed in this way, hledger-web saves a+numbered backup (assuming file permissions allow it, the disk is not+full, etc.) hledger-web is not aware of version control systems,+currently; if you use one, you'll have to arrange to commit the changes+yourself (eg with a cron job or a file watcher like entr).++ Changes which would leave the journal file(s) unparseable or+non-valid (eg with failing balance assertions) are prevented.+(Probably. This needs re-testing.)+++File: hledger-web.info, Node: RELOADING, Next: JSON API, Prev: EDITING UPLOADING DOWNLOADING, Up: Top++4 RELOADING+***********++hledger-web detects changes made to the files by other means (eg if you+edit it directly, outside of hledger-web), and it will show the new data+when you reload the page or navigate to a new page. If a change makes a+file unparseable, hledger-web will display an error message until the+file has been fixed.+++File: hledger-web.info, Node: JSON API, Prev: RELOADING, Up: Top++5 JSON API+**********++In addition to the web UI, hledger-web provides some JSON API routes.+These are similar to the API provided by the hledger-api tool, but it+may be convenient to have them in hledger-web also.++/accountnames+/transactions+/prices+/commodities+/accounts+/accounttransactions/#AccountName++ Tag Table: Node: Top72-Node: OPTIONS3154-Ref: #options3239+Node: OPTIONS1354+Ref: #options1459+Node: PERMISSIONS6549+Ref: #permissions6688+Node: EDITING UPLOADING DOWNLOADING7900+Ref: #editing-uploading-downloading8081+Node: RELOADING8915+Ref: #reloading9049+Node: JSON API9359+Ref: #json-api9453 End Tag Table
hledger-web.txt view
@@ -35,45 +35,13 @@ C:/Users/USER/.hledger.journal). For more about this see hledger(1), hledger_journal(5) etc. - By default, hledger-web starts the web app in "transient mode" and also- opens it in your default web browser if possible. In this mode the web- app will keep running for as long as you have it open in a browser win-- dow, and will exit after two minutes of inactivity (no requests and no- browser windows viewing it). With --serve, it just runs the web app- without exiting, and logs requests to the console.-- By default the server listens on IP address 127.0.0.1, accessible only- to local requests. You can use --host to change this, eg- --host 0.0.0.0 to listen on all configured addresses.-- Similarly, use --port to set a TCP port other than 5000, eg if you are- running multiple hledger-web instances.-- You can use --base-url to change the protocol, hostname, port and path- that appear in hyperlinks, useful eg for integrating hledger-web within- a larger website. The default is http://HOST:PORT/ using the server's- configured host address and TCP port (or http://HOST if PORT is 80).-- With --file-url you can set a different base url for static files, eg- for better caching or cookie-less serving on high performance websites.-- Note there is no built-in access control (aside from listening on- 127.0.0.1 by default). So you will need to hide hledger-web behind an- authenticating proxy (such as apache or nginx) if you want to restrict- who can see and add entries to your journal.-+OPTIONS Command-line options and arguments may be used to set an initial filter- on the data. This is not shown in the web UI, but it will be applied- in addition to any search query entered there.-- With journal and timeclock files (but not CSV files, currently) the web- app detects changes made by other means and will show the new data on- the next request. If a change makes the file unparseable, hledger-web- will show an error until the file has been fixed.+ on the data. These filter options are not shown in the web UI, but it+ will be applied in addition to any search query entered there. -OPTIONS- Note: if invoking hledger-web as a hledger subcommand, write -- before- options as shown above.+ Note: if invoking hledger-web as a hledger subcommand, write -- before+ options, as shown in the synopsis above. --serve serve and log requests, don't browse or auto-exit@@ -85,16 +53,24 @@ listen on this TCP port (default: 5000) --base-url=URL- set the base url (default: http://IPADDR:PORT). You would+ set the base url (default: http://IPADDR:PORT). You would change this when sharing over the network, or integrating within a larger website. --file-url=URL set the static files url (default: BASEURL/static). hledger-web- normally serves static files itself, but if you wanted to serve- them from another server for efficiency, you would set the url+ normally serves static files itself, but if you wanted to serve+ them from another server for efficiency, you would set the url with this. + --capabilities=CAP[,CAP..]+ enable the view, add, and/or manage capabilities (default:+ view,add)++ --capabilities-header=HTTPHEADER+ read capabilities to enable from a HTTP header, like X-Sand-+ storm-Permissions (default: disabled)+ hledger input options: -f FILE --file=FILE@@ -102,7 +78,7 @@ $LEDGER_FILE or $HOME/.hledger.journal) --rules-file=RULESFILE- Conversion rules file to use when reading CSV (default:+ Conversion rules file to use when reading CSV (default: FILE.rules) --separator=CHAR@@ -143,11 +119,11 @@ multiperiod/multicolumn report by year -p --period=PERIODEXP- set start date, end date, and/or reporting interval all at once+ set start date, end date, and/or reporting interval all at once using period expressions syntax (overrides the flags above) --date2- match the secondary date instead (see command help for other+ match the secondary date instead (see command help for other effects) -U --unmarked@@ -166,21 +142,21 @@ hide/aggregate accounts or postings more than NUM levels deep -E --empty- show items with zero amount, normally hidden (and vice-versa in+ show items with zero amount, normally hidden (and vice-versa in hledger-ui/hledger-web) -B --cost- convert amounts to their cost at transaction time (using the+ convert amounts to their cost at transaction time (using the transaction price, if any) -V --value- convert amounts to their market value on the report end date+ convert amounts to their market value on the report end date (using the most recent applicable market price, if any) --auto apply automated posting rules to modify transactions. --forecast- apply periodic transaction rules to generate future transac-+ apply periodic transaction rules to generate future transac- tions, to 6 months from now or report end date. When a reporting option appears more than once in the command line, the@@ -200,22 +176,114 @@ show debug output (levels 1-9, default: 1) A @FILE argument will be expanded to the contents of FILE, which should- contain one command line option/argument per line. (To prevent this,+ contain one command line option/argument per line. (To prevent this, insert a -- argument before.) + By default, hledger-web starts the web app in "transient mode" and also+ opens it in your default web browser if possible. In this mode the web+ app will keep running for as long as you have it open in a browser win-+ dow, and will exit after two minutes of inactivity (no requests and no+ browser windows viewing it). With --serve, it just runs the web app+ without exiting, and logs requests to the console.++ By default the server listens on IP address 127.0.0.1, accessible only+ to local requests. You can use --host to change this, eg+ --host 0.0.0.0 to listen on all configured addresses.++ Similarly, use --port to set a TCP port other than 5000, eg if you are+ running multiple hledger-web instances.++ You can use --base-url to change the protocol, hostname, port and path+ that appear in hyperlinks, useful eg for integrating hledger-web within+ a larger website. The default is http://HOST:PORT/ using the server's+ configured host address and TCP port (or http://HOST if PORT is 80).++ With --file-url you can set a different base url for static files, eg+ for better caching or cookie-less serving on high performance websites.++PERMISSIONS+ By default, hledger-web allows anyone who can reach it to view the+ journal and to add new transactions, but not to change existing data.++ You can restrict who can reach it by++ o setting the IP address it listens on (see --host above). By default+ it listens on 127.0.0.1, accessible to all users on the local+ machine.++ o putting it behind an authenticating proxy, using eg apache or nginx++ o custom firewall rules++ You can restrict what the users who reach it can do, by++ o using the --capabilities=CAP[,CAP..] flag when you start it, enabling+ one or more of the following capabilities. The default value is+ view,add:++ o view - allows viewing the journal file and all included files++ o add - allows adding new transactions to the main journal file++ o manage - allows editing, uploading or downloading the main or+ included files++ o using the --capabilities-header=HTTPHEADER flag to specify a HTTP+ header from which it will read capabilities to enable. hledger-web+ on Sandstorm uses the X-Sandstorm-Permissions header to integrate+ with Sandstorm's permissions. This is disabled by default.++EDITING, UPLOADING, DOWNLOADING+ If you enable the manage capability mentioned above, you'll see a new+ "spanner" button to the right of the search form. Clicking this will+ let you edit, upload, or download the journal file or any files it+ includes.++ Note, unlike any other hledger command, in this mode you (or any visi-+ tor) can alter or wipe the data files.++ Normally whenever a file is changed in this way, hledger-web saves a+ numbered backup (assuming file permissions allow it, the disk is not+ full, etc.) hledger-web is not aware of version control systems, cur-+ rently; if you use one, you'll have to arrange to commit the changes+ yourself (eg with a cron job or a file watcher like entr).++ Changes which would leave the journal file(s) unparseable or non-valid+ (eg with failing balance assertions) are prevented. (Probably. This+ needs re-testing.)++RELOADING+ hledger-web detects changes made to the files by other means (eg if you+ edit it directly, outside of hledger-web), and it will show the new+ data when you reload the page or navigate to a new page. If a change+ makes a file unparseable, hledger-web will display an error message+ until the file has been fixed.++JSON API+ In addition to the web UI, hledger-web provides some JSON API routes.+ These are similar to the API provided by the hledger-api tool, but it+ may be convenient to have them in hledger-web also.++ /accountnames+ /transactions+ /prices+ /commodities+ /accounts+ /accounttransactions/#AccountName+ ENVIRONMENT LEDGER_FILE The journal file path when not specified with -f. Default:- ~/.hledger.journal (on windows, perhaps C:/Users/USER/.hledger.jour-+ ~/.hledger.journal (on windows, perhaps C:/Users/USER/.hledger.jour- nal). FILES- Reads data from one or more files in hledger journal, timeclock, time-- dot, or CSV format specified with -f, or $LEDGER_FILE, or- $HOME/.hledger.journal (on windows, perhaps+ Reads data from one or more files in hledger journal, timeclock, time-+ dot, or CSV format specified with -f, or $LEDGER_FILE, or+ $HOME/.hledger.journal (on windows, perhaps C:/Users/USER/.hledger.journal). BUGS- The need to precede options with -- when invoked from hledger is awk-+ The need to precede options with -- when invoked from hledger is awk- ward. -f- doesn't work (hledger-web can't read from stdin).@@ -229,7 +297,7 @@ REPORTING BUGS- Report bugs at http://bugs.hledger.org (or on the #hledger IRC channel+ Report bugs at http://bugs.hledger.org (or on the #hledger IRC channel or hledger mail list) @@ -243,7 +311,7 @@ SEE ALSO- hledger(1), hledger-ui(1), hledger-web(1), hledger-api(1),+ hledger(1), hledger-ui(1), hledger-web(1), hledger-api(1), hledger_csv(5), hledger_journal(5), hledger_timeclock(5), hledger_time- dot(5), ledger(1) @@ -251,4 +319,4 @@ -hledger-web 1.13 February 2019 hledger-web(1)+hledger-web 1.14 March 2019 hledger-web(1)
templates/add-form.hamlet view
@@ -40,7 +40,7 @@ <div .col-md-9 .col-xs-6 .col-sm-6> <div .account-postings>- $forall (n, (acc, amt, accE, amtE)) <- msgs+ $forall (n, (acc, amt, accE, amtE)) <- displayRows <div .form-group .row .account-group> <div .col-md-8 .col-xs-8 .col-sm-8 :isJust accE:.has-error> <input .account-input.form-control.input-lg.typeahead type=text
templates/chart.hamlet view
@@ -38,7 +38,7 @@ #{simpleMixedAmountQuantity $ triCommodityBalance c i}, '#{showMixedAmountWithZeroCommodity $ triCommodityAmount c i}', '#{showMixedAmountWithZeroCommodity $ triCommodityBalance c i}',- '#{concat $ intersperse "\\n" $ lines $ show $ triOrigTransaction i}',+ '#{concat $ intersperse "\\n" $ lines $ showTransaction $ triOrigTransaction i}', #{tindex $ triOrigTransaction i} ], /* [] */
templates/journal.hamlet view
@@ -15,7 +15,7 @@ <th .amount style="text-align:right;">Amount $forall (torig, _, split, _, amt, _) <- items- <tr .title #transaction-#{tindex torig}>+ <tr .title #transaction-#{tindex torig} title="#{showTransaction torig}"> <td .date nowrap> #{show (tdate torig)} <td colspan=2>@@ -25,7 +25,7 @@ ^{mixedAmountAsHtml amt} $forall Posting { paccount = acc, pamount = amt } <- tpostings torig- <tr .posting title="#{show torig}">+ <tr .posting> <td> <td> <td>
templates/register.hamlet view
@@ -19,11 +19,11 @@ <tbody> $forall (torig, tacct, split, acct, amt, bal) <- items- <tr ##{tindex torig} title="#{show torig}" style="vertical-align:top;">+ <tr ##{tindex torig} title="#{showTransaction torig}" style="vertical-align:top;"> <td .date> <a href="@{JournalR}#transaction-#{tindex torig}"> #{show (tdate tacct)}- <td title="#{show torig}">+ <td> #{textElideRight 30 (tdescription tacct)} <td .account> #{elideRight 40 acct}