web: offer a choice of files to edit when there are multiple (included) files

This commit is contained in:
Simon Michael 2010-09-24 01:56:11 +00:00
parent 78db98366f
commit 7714bab58d
10 changed files with 165 additions and 94 deletions

View File

@ -10,6 +10,7 @@ module Hledger.Data.Journal
where where
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Map (findWithDefault, (!)) import Data.Map (findWithDefault, (!))
import Safe (headDef)
import System.Time (ClockTime(TOD)) import System.Time (ClockTime(TOD))
import Hledger.Data.Utils import Hledger.Data.Utils
import Hledger.Data.Types import Hledger.Data.Types
@ -24,7 +25,7 @@ import Hledger.Data.TimeLog
instance Show Journal where instance Show Journal where
show j = printf "Journal %s with %d transactions, %d accounts: %s" show j = printf "Journal %s with %d transactions, %d accounts: %s"
(filepath j) (journalFilePath j)
(length (jtxns j) + (length (jtxns j) +
length (jmodifiertxns j) + length (jmodifiertxns j) +
length (jperiodictxns j)) length (jperiodictxns j))
@ -40,10 +41,8 @@ nulljournal = Journal { jmodifiertxns = []
, open_timelog_entries = [] , open_timelog_entries = []
, historical_prices = [] , historical_prices = []
, final_comment_lines = [] , final_comment_lines = []
, filepath = "" , files = []
, allfilepaths = []
, filereadtime = TOD 0 0 , filereadtime = TOD 0 0
, jtext = ""
} }
nullfilterspec = FilterSpec { nullfilterspec = FilterSpec {
@ -58,6 +57,15 @@ nullfilterspec = FilterSpec {
,depth=Nothing ,depth=Nothing
} }
journalFilePath :: Journal -> FilePath
journalFilePath = fst . mainfile
journalFilePaths :: Journal -> [FilePath]
journalFilePaths = map fst . files
mainfile :: Journal -> (FilePath, String)
mainfile = headDef ("", "") . files
addTransaction :: Transaction -> Journal -> Journal addTransaction :: Transaction -> Journal -> Journal
addTransaction t l0 = l0 { jtxns = t : jtxns l0 } addTransaction t l0 = l0 { jtxns = t : jtxns l0 }
@ -214,10 +222,11 @@ journalSelectingDate EffectiveDate j =
-- | Do post-parse processing on a journal, to make it ready for use. -- | Do post-parse processing on a journal, to make it ready for use.
journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> Journal -> Journal journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> Journal -> Journal
journalFinalise tclock tlocal path txt j = journalCanonicaliseAmounts $ journalFinalise tclock tlocal path txt j@Journal{files=fs} =
journalApplyHistoricalPrices $ journalCanonicaliseAmounts $
journalCloseTimeLogEntries tlocal journalApplyHistoricalPrices $
j{filepath=path, allfilepaths=path:(allfilepaths j), filereadtime=tclock, jtext=txt} journalCloseTimeLogEntries tlocal
j{files=(path,txt):fs, filereadtime=tclock}
-- | Convert all the journal's amounts to their canonical display -- | Convert all the journal's amounts to their canonical display
-- settings. Ie, all amounts in a given commodity will use (a) the -- settings. Ie, all amounts in a given commodity will use (a) the

View File

@ -128,10 +128,11 @@ data Journal = Journal {
open_timelog_entries :: [TimeLogEntry], open_timelog_entries :: [TimeLogEntry],
historical_prices :: [HistoricalPrice], historical_prices :: [HistoricalPrice],
final_comment_lines :: String, -- ^ any trailing comments from the journal file final_comment_lines :: String, -- ^ any trailing comments from the journal file
filepath :: FilePath, -- ^ file path of this journal files :: [(FilePath, String)], -- ^ the file path and raw text of the main and
allfilepaths :: [FilePath], -- ^ file paths of this and any included journals -- any included journal files. The main file is
filereadtime :: ClockTime, -- ^ when this journal was read from its file -- first followed by any included files in the
jtext :: String -- ^ the raw text read from the journal's file -- order encountered.
filereadtime :: ClockTime -- ^ when this journal was last read from its file(s)
} deriving (Eq, Typeable) } deriving (Eq, Typeable)
data Ledger = Ledger { data Ledger = Ledger {

View File

@ -26,7 +26,6 @@ module Test.HUnit,
where where
import Data.Char import Data.Char
import Codec.Binary.UTF8.String as UTF8 (decodeString, encodeString, isUTF8Encoded) import Codec.Binary.UTF8.String as UTF8 (decodeString, encodeString, isUTF8Encoded)
import Control.Exception
import Control.Monad import Control.Monad
import Data.List import Data.List
--import qualified Data.Map as Map --import qualified Data.Map as Map
@ -361,9 +360,6 @@ isLeft _ = False
isRight :: Either a b -> Bool isRight :: Either a b -> Bool
isRight = not . isLeft isRight = not . isLeft
strictReadFile :: FilePath -> IO String
strictReadFile f = readFile f >>= \s -> Control.Exception.evaluate (length s) >> return s
-- -- | Expand ~ in a file path (does not handle ~name). -- -- | Expand ~ in a file path (does not handle ~name).
-- tildeExpand :: FilePath -> IO FilePath -- tildeExpand :: FilePath -> IO FilePath
-- tildeExpand ('~':[]) = getHomeDirectory -- tildeExpand ('~':[]) = getHomeDirectory

View File

@ -107,6 +107,7 @@ module Hledger.Read.Journal (
tests_Journal, tests_Journal,
reader, reader,
journalFile, journalFile,
journalAddFile,
someamount, someamount,
ledgeraccountname, ledgeraccountname,
ledgerExclamationDirective, ledgerExclamationDirective,
@ -150,7 +151,9 @@ detect f _ = fileSuffix f == format
-- | Parse and post-process a "Journal" from hledger's journal file -- | Parse and post-process a "Journal" from hledger's journal file
-- format, or give an error. -- format, or give an error.
parse :: FilePath -> String -> ErrorT String IO Journal parse :: FilePath -> String -> ErrorT String IO Journal
parse = parseJournalWith journalFile parse = do
j <- parseJournalWith journalFile
return j
-- | Top-level journal parser. Returns a single composite, I/O performing, -- | Top-level journal parser. Returns a single composite, I/O performing,
-- error-raising "JournalUpdate" which can be applied to an empty journal -- error-raising "JournalUpdate" which can be applied to an empty journal
@ -176,8 +179,8 @@ journalFile = do journalupdates <- many journalItem
, emptyLine >> return (return id) , emptyLine >> return (return id)
] <?> "journal transaction or directive" ] <?> "journal transaction or directive"
journalAddFilePath :: FilePath -> Journal -> Journal journalAddFile :: (FilePath,String) -> Journal -> Journal
journalAddFilePath f j@Journal{allfilepaths=fs} = j{allfilepaths=fs++[f]} journalAddFile f j@Journal{files=fs} = j{files=fs++[f]}
emptyLine :: GenParser Char JournalContext () emptyLine :: GenParser Char JournalContext ()
emptyLine = do many spacenonewline emptyLine = do many spacenonewline
@ -218,10 +221,10 @@ ledgerInclude = do
outerState <- getState outerState <- getState
outerPos <- getPosition outerPos <- getPosition
return $ do filepath <- expandPath outerPos filename return $ do filepath <- expandPath outerPos filename
contents <- readFileOrError outerPos filepath txt <- readFileOrError outerPos filepath
let inIncluded = show outerPos ++ " in included file " ++ show filename ++ ":\n" let inIncluded = show outerPos ++ " in included file " ++ show filename ++ ":\n"
case runParser journalFile outerState filepath contents of case runParser journalFile outerState filepath txt of
Right ju -> juSequence [return $ journalAddFilePath filepath, ju] `catchError` (throwError . (inIncluded ++)) Right ju -> juSequence [return $ journalAddFile (filepath,txt), ju] `catchError` (throwError . (inIncluded ++))
Left err -> throwError $ inIncluded ++ show err Left err -> throwError $ inIncluded ++ show err
where readFileOrError pos fp = where readFileOrError pos fp =
ErrorT $ liftM Right (readFile fp) `catch` ErrorT $ liftM Right (readFile fp) `catch`

View File

@ -27,7 +27,7 @@ import Yesod.Helpers.Auth
import Text.Hamlet (defaultHamletSettings) import Text.Hamlet (defaultHamletSettings)
import Text.Hamlet.RT import Text.Hamlet.RT
import Hledger.Cli.Commands.Add (journalAddTransaction) import Hledger.Cli.Commands.Add (appendToJournalFile)
import Hledger.Cli.Commands.Balance import Hledger.Cli.Commands.Balance
import Hledger.Cli.Commands.Print import Hledger.Cli.Commands.Print
import Hledger.Cli.Commands.Register import Hledger.Cli.Commands.Register
@ -339,7 +339,7 @@ navbar TD{p=p,j=j,today=today} = [$hamlet|
journalTitleDesc :: Journal -> String -> Day -> (String, String) journalTitleDesc :: Journal -> String -> Day -> (String, String)
journalTitleDesc j p today = (title, desc) journalTitleDesc j p today = (title, desc)
where where
title = printf "%s" (takeFileName $ filepath j) :: String title = printf "%s" (takeFileName $ journalFilePath j) :: String
desc = printf "%s" (showspan span) :: String desc = printf "%s" (showspan span) :: String
span = either (const $ DateSpan Nothing Nothing) snd (parsePeriodExpr today p) span = either (const $ DateSpan Nothing Nothing) snd (parsePeriodExpr today p)
showspan (DateSpan Nothing Nothing) = "" showspan (DateSpan Nothing Nothing) = ""
@ -503,7 +503,7 @@ getJournalR = do
br = balanceReportAsHtml opts td $ balanceReport opts fspec j br = balanceReportAsHtml opts td $ balanceReport opts fspec j
jr = journalReportAsHtml opts td $ journalReport opts fspec j jr = journalReportAsHtml opts td $ journalReport opts fspec j
td = mktd{here=here, title="hledger journal", msg=msg, a=a, p=p, j=j, today=today} td = mktd{here=here, title="hledger journal", msg=msg, a=a, p=p, j=j, today=today}
editform' = editform td $ jtext j editform' = editform td
hamletToRepHtml $ pageLayout td [$hamlet| hamletToRepHtml $ pageLayout td [$hamlet|
%div.ledger %div.ledger
%div.accounts!style=float:left; ^br^ %div.accounts!style=float:left; ^br^
@ -534,7 +534,7 @@ getRegisterR = do
br = balanceReportAsHtml opts td $ balanceReport opts fspec j br = balanceReportAsHtml opts td $ balanceReport opts fspec j
rr = registerReportAsHtml opts td $ registerReport opts fspec j rr = registerReportAsHtml opts td $ registerReport opts fspec j
td = mktd{here=here, title="hledger register", msg=msg, a=a, p=p, j=j, today=today} td = mktd{here=here, title="hledger register", msg=msg, a=a, p=p, j=j, today=today}
editform' = editform td $ jtext j editform' = editform td
hamletToRepHtml $ pageLayout td [$hamlet| hamletToRepHtml $ pageLayout td [$hamlet|
%div.ledger %div.ledger
%div.accounts!style=float:left; ^br^ %div.accounts!style=float:left; ^br^
@ -635,7 +635,7 @@ getJournalOnlyR = do
(a, p, opts, fspec, j, msg, here) <- getHandlerData (a, p, opts, fspec, j, msg, here) <- getHandlerData
today <- liftIO getCurrentDay today <- liftIO getCurrentDay
let td = mktd{here=here, title="hledger journal", msg=msg, a=a, p=p, j=j, today=today} let td = mktd{here=here, title="hledger journal", msg=msg, a=a, p=p, j=j, today=today}
editform' = editform td $ jtext j editform' = editform td
txns = journalReportAsHtml opts td $ journalReport opts fspec j txns = journalReportAsHtml opts td $ journalReport opts fspec j
hamletToRepHtml $ pageLayout td [$hamlet| hamletToRepHtml $ pageLayout td [$hamlet|
%div.journal %div.journal
@ -714,6 +714,8 @@ addform td = [$hamlet|
%td!colspan=4 %td!colspan=4
%input!type=hidden!name=action!value=add %input!type=hidden!name=action!value=add
%input!type=submit!name=submit!value="add transaction" %input!type=submit!name=submit!value="add transaction"
$if manyfiles
\ to: ^journalselect.files.j.td^
|] |]
where where
-- datehelplink = helplink "dates" "..." -- datehelplink = helplink "dates" "..."
@ -721,6 +723,7 @@ addform td = [$hamlet|
deschelp = "eg: supermarket (optional)" deschelp = "eg: supermarket (optional)"
date = "today" date = "today"
descriptions = sort $ nub $ map tdescription $ jtxns $ j td descriptions = sort $ nub $ map tdescription $ jtxns $ j td
manyfiles = (length $ files $ j td) > 1
postingsfields :: TemplateData -> Hamlet AppRoute postingsfields :: TemplateData -> Hamlet AppRoute
postingsfields td = [$hamlet| postingsfields td = [$hamlet|
@ -732,7 +735,7 @@ postingsfields td = [$hamlet|
p2 = postingfields td 2 p2 = postingfields td 2
postingfields :: TemplateData -> Int -> Hamlet AppRoute postingfields :: TemplateData -> Int -> Hamlet AppRoute
postingfields td n = [$hamlet| postingfields TD{j=j} n = [$hamlet|
%tr#postingrow %tr#postingrow
%td!align=right $acctlabel$: %td!align=right $acctlabel$:
%td %td
@ -753,7 +756,7 @@ postingfields td n = [$hamlet|
numbered = (++ show n) numbered = (++ show n)
acctvar = numbered "account" acctvar = numbered "account"
amtvar = numbered "amount" amtvar = numbered "amount"
acctnames = sort $ journalAccountNamesUsed $ j td acctnames = sort $ journalAccountNamesUsed j
(acctlabel, accthelp, amtfield, amthelp) (acctlabel, accthelp, amtfield, amthelp)
| n == 1 = ("To account" | n == 1 = ("To account"
,"eg: expenses:food" ,"eg: expenses:food"
@ -771,14 +774,19 @@ postingfields td n = [$hamlet|
,"" ,""
) )
editform :: TemplateData -> String -> Hamlet AppRoute editform :: TemplateData -> Hamlet AppRoute
editform _ content = [$hamlet| editform TD{j=j} = [$hamlet|
%form#editform!method=POST!style=display:none; %form#editform!method=POST!style=display:none;
%table.form#editform %table.form#editform
$if manyfiles
%tr
%td!colspan=2
Editing ^journalselect.files.j^
%tr %tr
%td!colspan=2 %td!colspan=2
%textarea!name=text!rows=30!cols=80 $forall files.j f
$content$ %textarea!id=$fst.f$_textarea!name=text!rows=25!cols=80!style=display:none;!disabled=disabled
$snd.f$
%tr#addbuttonrow %tr#addbuttonrow
%td %td
%span.help ^formathelp^ %span.help ^formathelp^
@ -788,10 +796,18 @@ editform _ content = [$hamlet|
%input!type=submit!name=submit!value="save journal" %input!type=submit!name=submit!value="save journal"
\ or $ \ or $
%a!href!onclick="return editformToggle()" cancel %a!href!onclick="return editformToggle()" cancel
|] |] -- XXX textarea ids are unquoted journal file paths, which is not valid html
where where
manyfiles = (length $ files j) > 1
formathelp = helplink "file-format" "file format help" formathelp = helplink "file-format" "file format help"
journalselect :: [(FilePath,String)] -> Hamlet AppRoute
journalselect journalfiles = [$hamlet|
%select!id=journalselect!name=journal!onchange="editformJournalSelect()"
$forall journalfiles f
%option!value=$fst.f$ $fst.f$
|]
importform :: Hamlet AppRoute importform :: Hamlet AppRoute
importform = [$hamlet| importform = [$hamlet|
%form#importform!method=POST!style=display:none; %form#importform!method=POST!style=display:none;
@ -815,17 +831,18 @@ postJournalOnlyR = do
-- | Handle a journal add form post. -- | Handle a journal add form post.
postAddForm :: Handler RepPlain postAddForm :: Handler RepPlain
postAddForm = do postAddForm = do
(_, _, opts, _, _, _, _) <- getHandlerData (_, _, _, _, j, _, _) <- getHandlerData
today <- liftIO getCurrentDay today <- liftIO getCurrentDay
-- get form input values. M means a Maybe value. -- get form input values. M means a Maybe value.
(dateM, descM, acct1M, amt1M, acct2M, amt2M) <- runFormPost' (dateM, descM, acct1M, amt1M, acct2M, amt2M, journalM) <- runFormPost'
$ (,,,,,) $ (,,,,,,)
<$> maybeStringInput "date" <$> maybeStringInput "date"
<*> maybeStringInput "description" <*> maybeStringInput "description"
<*> maybeStringInput "account1" <*> maybeStringInput "account1"
<*> maybeStringInput "amount1" <*> maybeStringInput "amount1"
<*> maybeStringInput "account2" <*> maybeStringInput "account2"
<*> maybeStringInput "amount2" <*> maybeStringInput "amount2"
<*> maybeStringInput "journal"
-- supply defaults and parse date and amounts, or get errors. -- supply defaults and parse date and amounts, or get errors.
let dateE = maybe (Left "date required") (either (\e -> Left $ showDateParseError e) Right . fixSmartDateStrEither today) dateM let dateE = maybe (Left "date required") (either (\e -> Left $ showDateParseError e) Right . fixSmartDateStrEither today) dateM
descE = Right $ fromMaybe "" descM descE = Right $ fromMaybe "" descM
@ -833,11 +850,16 @@ postAddForm = do
acct2E = maybe (Left "from account required") Right acct2M acct2E = maybe (Left "from account required") Right acct2M
amt1E = maybe (Left "amount required") (either (const $ Left "could not parse amount") Right . parse someamount "") amt1M amt1E = maybe (Left "amount required") (either (const $ Left "could not parse amount") Right . parse someamount "") amt1M
amt2E = maybe (Right missingamt) (either (const $ Left "could not parse amount") Right . parse someamount "") amt2M amt2E = maybe (Right missingamt) (either (const $ Left "could not parse amount") Right . parse someamount "") amt2M
strEs = [dateE, descE, acct1E, acct2E] journalE = maybe (Right $ journalFilePath j)
(\f -> if f `elem` journalFilePaths j
then Right f
else Left $ "unrecognised journal file path: " ++ f)
journalM
strEs = [dateE, descE, acct1E, acct2E, journalE]
amtEs = [amt1E, amt2E] amtEs = [amt1E, amt2E]
[date,desc,acct1,acct2] = rights strEs
[amt1,amt2] = rights amtEs
errs = lefts strEs ++ lefts amtEs errs = lefts strEs ++ lefts amtEs
[date,desc,acct1,acct2,journalpath] = rights strEs
[amt1,amt2] = rights amtEs
-- if no errors so far, generate a transaction and balance it or get the error. -- if no errors so far, generate a transaction and balance it or get the error.
tE | not $ null errs = Left errs tE | not $ null errs = Left errs
| otherwise = either (\e -> Left ["unbalanced postings: " ++ (head $ lines e)]) Right | otherwise = either (\e -> Left ["unbalanced postings: " ++ (head $ lines e)]) Right
@ -863,46 +885,54 @@ postAddForm = do
Right t -> do Right t -> do
let t' = txnTieKnot t -- XXX move into balanceTransaction let t' = txnTieKnot t -- XXX move into balanceTransaction
j <- liftIO $ fromJust `fmap` getValue "hledger" "journal" liftIO $ appendToJournalFile journalpath $ showTransaction t'
liftIO $ journalAddTransaction j opts t'
setMessage $ string $ printf "Added transaction:\n%s" (show t') setMessage $ string $ printf "Added transaction:\n%s" (show t')
redirect RedirectTemporary RegisterR redirect RedirectTemporary RegisterR
-- | Handle a journal edit form post. -- | Handle a journal edit form post.
postEditForm :: Handler RepPlain postEditForm :: Handler RepPlain
postEditForm = do postEditForm = do
-- get form input values, or basic validation errors. E means an Either value. (_, _, _, _, j, _, _) <- getHandlerData
textM <- runFormPost' $ maybeStringInput "text" -- get form input values, or validation errors.
-- getRequest >>= liftIO (reqRequestBody req) >>= mtrace
(textM, journalM) <- runFormPost'
$ (,)
<$> maybeStringInput "text"
<*> maybeStringInput "journal"
let textE = maybe (Left "No value provided") Right textM let textE = maybe (Left "No value provided") Right textM
-- display errors or add transaction journalE = maybe (Right $ journalFilePath j)
case textE of (\f -> if f `elem` journalFilePaths j
Left errs -> do then Right f
-- XXX should save current form values in session else Left "unrecognised journal file path")
setMessage $ string errs journalM
strEs = [textE, journalE]
errs = lefts strEs
[text,journalpath] = rights strEs
-- display errors or perform edit
if not $ null errs
then do
setMessage $ string $ intercalate "; " errs
redirect RedirectTemporary JournalR redirect RedirectTemporary JournalR
Right t' -> do else do
-- try to avoid unnecessary backups or saving invalid data -- try to avoid unnecessary backups or saving invalid data
j <- liftIO $ fromJust `fmap` getValue "hledger" "journal" filechanged' <- liftIO $ journalSpecifiedFileIsNewer j journalpath
filechanged' <- liftIO $ journalFileIsNewer j told <- liftIO $ readFileStrictly journalpath
let f = filepath j let tnew = filter (/= '\r') text
told = jtext j
tnew = filter (/= '\r') t'
changed = tnew /= told || filechanged' changed = tnew /= told || filechanged'
-- changed <- liftIO $ writeFileWithBackupIfChanged f t''
if not changed if not changed
then do then do
setMessage $ string $ "No change" setMessage $ string $ "No change"
redirect RedirectTemporary JournalR redirect RedirectTemporary JournalR
else do else do
jE <- liftIO $ journalFromPathAndString Nothing f tnew jE <- liftIO $ journalFromPathAndString Nothing journalpath tnew
either either
(\e -> do (\e -> do
setMessage $ string e setMessage $ string e
redirect RedirectTemporary JournalR) redirect RedirectTemporary JournalR)
(const $ do (const $ do
liftIO $ writeFileWithBackup f tnew liftIO $ writeFileWithBackup journalpath tnew
setMessage $ string $ printf "Saved journal %s\n" (show f) setMessage $ string $ printf "Saved journal %s\n" (show journalpath)
redirect RedirectTemporary JournalR) redirect RedirectTemporary JournalR)
jE jE

View File

@ -70,6 +70,7 @@ function addformToggle() {
function editformToggle() { function editformToggle() {
var a = document.getElementById('addform'); var a = document.getElementById('addform');
var e = document.getElementById('editform'); var e = document.getElementById('editform');
var ej = document.getElementById('journalselect');
var f = document.getElementById('filterform'); var f = document.getElementById('filterform');
var i = document.getElementById('importform'); var i = document.getElementById('importform');
var t = document.getElementById('transactions'); var t = document.getElementById('transactions');
@ -87,9 +88,10 @@ function editformToggle() {
jlink.style['font-weight'] = 'normal'; jlink.style['font-weight'] = 'normal';
rlink.style['font-weight'] = 'normal'; rlink.style['font-weight'] = 'normal';
a.style.display = 'none'; a.style.display = 'none';
e.style.display = 'block';
i.style.display = 'none'; i.style.display = 'none';
t.style.display = 'none'; t.style.display = 'none';
e.style.display = 'block';
editformJournalSelect();
} else { } else {
alink.style['font-weight'] = 'normal'; alink.style['font-weight'] = 'normal';
elink.style['font-weight'] = 'normal'; elink.style['font-weight'] = 'normal';
@ -102,6 +104,24 @@ function editformToggle() {
return false; return false;
} }
function editformJournalSelect() {
var textareas = $('textarea', $('form#editform'));
for (i=0; i<textareas.length; i++) {
textareas[i].style.display = 'none';
textareas[i].disabled = true;
}
if (event.target.value) {
var journalid = event.target.value+'_textarea';
var textarea = document.getElementById(journalid);
}
else {
var textarea = textareas[0];
}
textarea.style.display = 'block';
textarea.disabled = false;
return true;
}
function importformToggle() { function importformToggle() {
var a = document.getElementById('addform'); var a = document.getElementById('addform');
var e = document.getElementById('editform'); var e = document.getElementById('editform');

View File

@ -36,7 +36,7 @@ add opts args j
++"To complete a transaction, enter . as account name. To quit, press control-c." ++"To complete a transaction, enter . as account name. To quit, press control-c."
today <- getCurrentDay today <- getCurrentDay
getAndAddTransactions j opts args today `catch` (\e -> unless (isEOFError e) $ ioError e) getAndAddTransactions j opts args today `catch` (\e -> unless (isEOFError e) $ ioError e)
where f = filepath j where f = journalFilePath j
-- | Read a number of transactions from the command line, prompting, -- | Read a number of transactions from the command line, prompting,
-- validating, displaying and appending them to the journal file, until -- validating, displaying and appending them to the journal file, until
@ -132,30 +132,28 @@ askFor prompt def validator = do
Nothing -> return input Nothing -> return input
where showdef s = " [" ++ s ++ "]" where showdef s = " [" ++ s ++ "]"
-- | Append this transaction to the journal's file. Also, to the journal's -- | Append this transaction to the journal's file, and to the journal's
-- transaction list, but we don't bother updating the other fields - this -- transaction list.
-- is enough to include new transactions in the history matching.
journalAddTransaction :: Journal -> [Opt] -> Transaction -> IO Journal journalAddTransaction :: Journal -> [Opt] -> Transaction -> IO Journal
journalAddTransaction j@Journal{jtxns=ts} opts t = do journalAddTransaction j@Journal{jtxns=ts} opts t = do
appendToJournalFile j $ showTransaction t let f = journalFilePath j
appendToJournalFile f $ showTransaction t
when (Debug `elem` opts) $ do when (Debug `elem` opts) $ do
putStrLn $ printf "\nAdded transaction to %s:" (filepath j) putStrLn $ printf "\nAdded transaction to %s:" f
putStrLn =<< registerFromString (show t) putStrLn =<< registerFromString (show t)
return j{jtxns=ts++[t]} return j{jtxns=ts++[t]}
-- | Append data to the journal's file, ensuring proper separation from -- | Append data to a journal file; or if the file is "-", dump it to stdout.
-- any existing data; or if the file is "-", dump it to stdout. appendToJournalFile :: FilePath -> String -> IO ()
appendToJournalFile :: Journal -> String -> IO () appendToJournalFile f s =
appendToJournalFile Journal{filepath=f, jtext=t} s =
if f == "-" if f == "-"
then putStr $ sep ++ s then putStr $ sep ++ s
else appendFile f $ sep++s else appendFile f $ sep++s
where where
-- XXX we are looking at the original raw text from when the journal sep = "\n\n"
-- was first read, but that's good enough for now -- sep | null $ strip t = ""
sep | null $ strip t = "" -- | otherwise = replicate (2 - min 2 (length lastnls)) '\n'
| otherwise = replicate (2 - min 2 (length lastnls)) '\n' -- where lastnls = takeWhile (=='\n') $ reverse t
where lastnls = takeWhile (=='\n') $ reverse t
-- | Convert a string of journal data into a register report. -- | Convert a string of journal data into a register report.
registerFromString :: String -> IO String registerFromString :: String -> IO String

View File

@ -38,7 +38,7 @@ showLedgerStats _ _ l today span =
w1 = maximum $ map (length . fst) stats w1 = maximum $ map (length . fst) stats
w2 = maximum $ map (length . show . snd) stats w2 = maximum $ map (length . show . snd) stats
stats = [ stats = [
("Journal file", filepath $ journal l) ("Journal file", journalFilePath $ journal l)
,("Transactions span", printf "%s to %s (%d days)" (start span) (end span) days) ,("Transactions span", printf "%s to %s (%d days)" (start span) (end span) days)
,("Last transaction", maybe "none" show lastdate ++ showelapsed lastelapsed) ,("Last transaction", maybe "none" show lastdate ++ showelapsed lastelapsed)
,("Transactions", printf "%d (%0.1f per day)" tnum txnrate) ,("Transactions", printf "%d (%0.1f per day)" tnum txnrate)

View File

@ -63,7 +63,7 @@ tests = TestList [
"account directive" ~: "account directive" ~:
let sameParse str1 str2 = do j1 <- readJournal Nothing str1 >>= either error' return let sameParse str1 str2 = do j1 <- readJournal Nothing str1 >>= either error' return
j2 <- readJournal Nothing str2 >>= either error' return j2 <- readJournal Nothing str2 >>= either error' return
j1 `is` j2{filereadtime=filereadtime j1, jtext=jtext j1} j1 `is` j2{filereadtime=filereadtime j1, files=files j1}
in TestList in TestList
[ [
"account directive 1" ~: sameParse "account directive 1" ~: sameParse
@ -1059,9 +1059,8 @@ journal7 = Journal
[] []
[] []
"" ""
"" []
(TOD 0 0) (TOD 0 0)
""
ledger7 = journalToLedger nullfilterspec journal7 ledger7 = journalToLedger nullfilterspec journal7
@ -1091,8 +1090,7 @@ journalWithAmounts as =
[] []
[] []
"" ""
"" []
(TOD 0 0) (TOD 0 0)
""
where parse = fromparse . parseWithCtx emptyCtx someamount where parse = fromparse . parseWithCtx emptyCtx someamount

View File

@ -13,15 +13,18 @@ module Hledger.Cli.Utils
journalReload, journalReload,
journalReloadIfChanged, journalReloadIfChanged,
journalFileIsNewer, journalFileIsNewer,
journalFileModificationTime, journalSpecifiedFileIsNewer,
fileModificationTime,
openBrowserOn, openBrowserOn,
writeFileWithBackup, writeFileWithBackup,
writeFileWithBackupIfChanged, writeFileWithBackupIfChanged,
readFileStrictly,
) )
where where
import Hledger.Data import Hledger.Data
import Hledger.Read import Hledger.Read
import Hledger.Cli.Options (Opt(..),journalFilePathFromOpts) -- ,optsToFilterSpec) import Hledger.Cli.Options (Opt(..),journalFilePathFromOpts) -- ,optsToFilterSpec)
import Control.Exception
import Safe (readMay) import Safe (readMay)
import System.Directory (getModificationTime, getDirectoryContents, copyFile) import System.Directory (getModificationTime, getDirectoryContents, copyFile)
import System.Exit import System.Exit
@ -38,7 +41,7 @@ withJournalDo opts args _ cmd = do
-- We kludgily read the file before parsing to grab the full text, unless -- We kludgily read the file before parsing to grab the full text, unless
-- it's stdin, or it doesn't exist and we are adding. We read it strictly -- it's stdin, or it doesn't exist and we are adding. We read it strictly
-- to let the add command work. -- to let the add command work.
journalFilePathFromOpts opts >>= readJournalFile Nothing >>= either (error'.trace "BBB") runcmd journalFilePathFromOpts opts >>= readJournalFile Nothing >>= either error' runcmd
where where
costify = (if CostBasis `elem` opts then journalConvertAmountsToCost else id) costify = (if CostBasis `elem` opts then journalConvertAmountsToCost else id)
runcmd = cmd opts args . costify runcmd = cmd opts args . costify
@ -52,7 +55,7 @@ readJournalWithOpts opts s = do
-- | Re-read a journal from its data file, or return an error string. -- | Re-read a journal from its data file, or return an error string.
journalReload :: Journal -> IO (Either String Journal) journalReload :: Journal -> IO (Either String Journal)
journalReload Journal{filepath=f} = readJournalFile Nothing f journalReload j = readJournalFile Nothing $ journalFilePath j
-- | Re-read a journal from its data file mostly, only if the file has -- | Re-read a journal from its data file mostly, only if the file has
-- changed since last read (or if there is no file, ie data read from -- changed since last read (or if there is no file, ie data read from
@ -60,26 +63,36 @@ journalReload Journal{filepath=f} = readJournalFile Nothing f
-- the error message while reading it, and a flag indicating whether it -- the error message while reading it, and a flag indicating whether it
-- was re-read or not. -- was re-read or not.
journalReloadIfChanged :: [Opt] -> Journal -> IO (Either String Journal, Bool) journalReloadIfChanged :: [Opt] -> Journal -> IO (Either String Journal, Bool)
journalReloadIfChanged opts j@Journal{filepath=f} = do journalReloadIfChanged opts j = do
changed <- journalFileIsNewer j let maybeChangedFilename f = do newer <- journalSpecifiedFileIsNewer j f
if changed return $ if newer then Just f else Nothing
changedfiles <- catMaybes `fmap` mapM maybeChangedFilename (journalFilePaths j)
if not $ null changedfiles
then do then do
when (Verbose `elem` opts) $ printf "%s has changed, reloading\n" f when (Verbose `elem` opts) $ printf "%s has changed, reloading\n" (head changedfiles)
jE <- journalReload j jE <- journalReload j
return (jE, True) return (jE, True)
else else
return (Right j, False) return (Right j, False)
-- | Has the journal's data file changed since last parsed ? -- | Has the journal's main data file changed since the journal was last
-- read ?
journalFileIsNewer :: Journal -> IO Bool journalFileIsNewer :: Journal -> IO Bool
journalFileIsNewer j@Journal{filereadtime=tread} = do journalFileIsNewer j@Journal{filereadtime=tread} = do
tmod <- journalFileModificationTime j tmod <- fileModificationTime $ journalFilePath j
return $ diffClockTimes tmod tread > (TimeDiff 0 0 0 0 0 0 0) return $ diffClockTimes tmod tread > (TimeDiff 0 0 0 0 0 0 0)
-- | Get the last modified time of the journal's data file (or if there is no -- | Has the specified file (presumably one of journal's data files)
-- file, the current time). -- changed since journal was last read ?
journalFileModificationTime :: Journal -> IO ClockTime journalSpecifiedFileIsNewer :: Journal -> FilePath -> IO Bool
journalFileModificationTime Journal{filepath=f} journalSpecifiedFileIsNewer Journal{filereadtime=tread} f = do
tmod <- fileModificationTime f
return $ diffClockTimes tmod tread > (TimeDiff 0 0 0 0 0 0 0)
-- | Get the last modified time of the specified file, or if it does not
-- exist or there is some other error, the current time.
fileModificationTime :: FilePath -> IO ClockTime
fileModificationTime f
| null f = getClockTime | null f = getClockTime
| otherwise = getModificationTime f `Prelude.catch` \_ -> getClockTime | otherwise = getModificationTime f `Prelude.catch` \_ -> getClockTime
@ -120,6 +133,9 @@ writeFileWithBackupIfChanged f t = do
writeFileWithBackup :: FilePath -> String -> IO () writeFileWithBackup :: FilePath -> String -> IO ()
writeFileWithBackup f t = backUpFile f >> writeFile f t writeFileWithBackup f t = backUpFile f >> writeFile f t
readFileStrictly :: FilePath -> IO String
readFileStrictly f = readFile f >>= \s -> Control.Exception.evaluate (length s) >> return s
-- | Back up this file with a (incrementing) numbered suffix, or give an error. -- | Back up this file with a (incrementing) numbered suffix, or give an error.
backUpFile :: FilePath -> IO () backUpFile :: FilePath -> IO ()
backUpFile fp = do backUpFile fp = do