packages feed

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 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}