summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.md28
-rw-r--r--Hledger/Web/Application.hs14
-rw-r--r--Hledger/Web/Handler/AddR.hs45
-rw-r--r--Hledger/Web/Handler/Common.hs38
-rw-r--r--Hledger/Web/Handler/MiscR.hs98
-rw-r--r--Hledger/Web/Handler/RegisterR.hs1
-rw-r--r--Hledger/Web/Json.hs160
-rw-r--r--Hledger/Web/Main.hs23
-rw-r--r--Hledger/Web/WebOptions.hs8
-rw-r--r--Hledger/Web/Widget/AddForm.hs139
-rw-r--r--config/routes9
-rw-r--r--hledger-web.1165
-rw-r--r--hledger-web.cabal23
-rw-r--r--hledger-web.info185
-rw-r--r--hledger-web.txt180
-rw-r--r--templates/add-form.hamlet2
-rw-r--r--templates/chart.hamlet2
-rw-r--r--templates/journal.hamlet4
-rw-r--r--templates/register.hamlet4
19 files changed, 840 insertions, 288 deletions
diff --git a/CHANGES.md b/CHANGES.md
index 3388e5a..528ed6b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -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
diff --git a/Hledger/Web/Application.hs b/Hledger/Web/Application.hs
index 7de1b16..07c546a 100644
--- a/Hledger/Web/Application.hs
+++ b/Hledger/Web/Application.hs
@@ -15,13 +15,13 @@ import Network.HTTP.Conduit (newManager)
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_))
diff --git a/Hledger/Web/Handler/AddR.hs b/Hledger/Web/Handler/AddR.hs
index 2689540..5b74cb9 100644
--- a/Hledger/Web/Handler/AddR.hs
+++ b/Hledger/Web/Handler/AddR.hs
@@ -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 @@ postAddR = do
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 @@ postAddR = do
<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
diff --git a/Hledger/Web/Handler/Common.hs b/Hledger/Web/Handler/Common.hs
deleted file mode 100644
index c77edf3..0000000
--- a/Hledger/Web/Handler/Common.hs
+++ /dev/null
@@ -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)
diff --git a/Hledger/Web/Handler/MiscR.hs b/Hledger/Web/Handler/MiscR.hs
new file mode 100644
index 0000000..da26eb3
--- /dev/null
+++ b/Hledger/Web/Handler/MiscR.hs
@@ -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
+
diff --git a/Hledger/Web/Handler/RegisterR.hs b/Hledger/Web/Handler/RegisterR.hs
index fafe091..5c663f4 100644
--- a/Hledger/Web/Handler/RegisterR.hs
+++ b/Hledger/Web/Handler/RegisterR.hs
@@ -58,3 +58,4 @@ dayToJsTimestamp d =
read (formatTime defaultTimeLocale "%s" t) * 1000 -- XXX read
where
t = UTCTime d (secondsToDiffTime 0)
+
diff --git a/Hledger/Web/Json.hs b/Hledger/Web/Json.hs
new file mode 100644
index 0000000..97d7250
--- /dev/null
+++ b/Hledger/Web/Json.hs
@@ -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)
diff --git a/Hledger/Web/Main.hs b/Hledger/Web/Main.hs
index 5e58dd8..4b53216 100644
--- a/Hledger/Web/Main.hs
+++ b/Hledger/Web/Main.hs
@@ -38,7 +38,7 @@ hledgerWebMain = do
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 @@ runWith opts
| "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 ()
diff --git a/Hledger/Web/WebOptions.hs b/Hledger/Web/WebOptions.hs
index a24f07a..d6f6411 100644
--- a/Hledger/Web/WebOptions.hs
+++ b/Hledger/Web/WebOptions.hs
@@ -54,13 +54,13 @@ webflags =
, 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)]
diff --git a/Hledger/Web/Widget/AddForm.hs b/Hledger/Web/Widget/AddForm.hs
index 47e85f0..893da53 100644
--- a/Hledger/Web/Widget/AddForm.hs
+++ b/Hledger/Web/Widget/AddForm.hs
@@ -21,7 +21,7 @@ import qualified Data.Text as T
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 @@ addForm j today = identifyForm "add" $ \extra -> do
(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 @@ addForm j today = identifyForm "add" $ \extra -> do
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 @@ addForm j today = identifyForm "add" $ \extra -> do
, 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)
-
- 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]})
-
- catPostings (t, t', Left (e, e')) xs = (t, t', e, e') : xs
- catPostings (t, t', Right _) xs = (t, t', Nothing, Nothing) : xs
-
- errorToFormMsg = first (("Invalid value: " <>) . T.pack . errorBundlePretty)
- validateAccount = errorToFormMsg . runParser (accountnamep <* eof) "" . T.strip
- validateAmount = errorToFormMsg . runParser (evalStateT (amountp <* eof) mempty) "" . T.strip
-
--- 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')
+ makeTransaction date desc postings =
+ nulltransaction {tdate = date, tdescription = desc, tpostings = postings}
+
+
+-- | 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
+
+ -- Zip accounts and amounts, fill in missing values and drop empty rows.
+ rows :: [(Text, Text)]
+ rows = filter (/= ("", "")) $ zipDefault "" (formSuccess [] acctRes) (formSuccess [] amtRes)
+
+ -- 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
+
+ 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
diff --git a/config/routes b/config/routes
index f33b124..4c47c63 100644
--- a/config/routes
+++ b/config/routes
@@ -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
diff --git a/hledger-web.1 b/hledger-web.1
index 95dc84b..99625a8 100644
--- a/hledger-web.1
+++ b/hledger-web.1
@@ -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 @@ timeclock, timedot, or CSV format specified with \f[C]\-f\f[], or
\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 @@ serve them from another server for efficiency, you would set the url
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 @@ 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, 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
diff --git a/hledger-web.cabal b/hledger-web.cabal
index 0065222..5a23812 100644
--- a/hledger-web.cabal
+++ b/hledger-web.cabal
@@ -4,10 +4,10 @@ cabal-version: 1.12
--
-- 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 @@ author: Simon Michael <simon@joyful.com>
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 @@ library
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 @@ library
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 @@ library
, 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 @@ executable hledger-web
hs-source-dirs:
app
ghc-options: -Wall -fwarn-tabs
- cpp-options: -DVERSION="1.13"
+ cpp-options: -DVERSION="1.14"
build-depends:
base
, hledger-web
diff --git a/hledger-web.info b/hledger-web.info
index 4b561b6..4e977ec 100644
--- a/hledger-web.info
+++ b/hledger-web.info
@@ -3,7 +3,7 @@ This is hledger-web.info, produced by makeinfo version 6.5 from stdin.

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,56 +25,26 @@ journal, timeclock, timedot, or CSV format specified with '-f', or
'$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'
@@ -96,6 +66,14 @@ options as shown above.
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:
@@ -209,10 +187,129 @@ the last one takes precedence.
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
diff --git a/hledger-web.txt b/hledger-web.txt
index 7f5ecb3..944073b 100644
--- a/hledger-web.txt
+++ b/hledger-web.txt
@@ -35,45 +35,13 @@ DESCRIPTION
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.
+ 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.
- 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.
-
-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 @@ OPTIONS
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 @@ OPTIONS
$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 @@ OPTIONS
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 @@ OPTIONS
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 @@ OPTIONS
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 @@ BUGS
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 @@ COPYRIGHT
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 @@ SEE ALSO
-hledger-web 1.13 February 2019 hledger-web(1)
+hledger-web 1.14 March 2019 hledger-web(1)
diff --git a/templates/add-form.hamlet b/templates/add-form.hamlet
index 4faa7d9..0743b18 100644
--- a/templates/add-form.hamlet
+++ b/templates/add-form.hamlet
@@ -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
diff --git a/templates/chart.hamlet b/templates/chart.hamlet
index 347490d..7c32556 100644
--- a/templates/chart.hamlet
+++ b/templates/chart.hamlet
@@ -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}
],
/* [] */
diff --git a/templates/journal.hamlet b/templates/journal.hamlet
index 6608e9a..a8f936a 100644
--- a/templates/journal.hamlet
+++ b/templates/journal.hamlet
@@ -15,7 +15,7 @@ $if elem CapAdd caps
<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 @@ $if elem CapAdd caps
^{mixedAmountAsHtml amt}
$forall Posting { paccount = acc, pamount = amt } <- tpostings torig
- <tr .posting title="#{show torig}">
+ <tr .posting>
<td>
<td>
<td>
diff --git a/templates/register.hamlet b/templates/register.hamlet
index 6b34872..54241ab 100644
--- a/templates/register.hamlet
+++ b/templates/register.hamlet
@@ -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}