hledger/hledger-web/Handler/AddForm.hs
Simon Michael 0f5ee154c4 lib: simplify parsers; cleanups (#275)
The journal/timeclock/timedot parsers, instead of constructing (opaque)
journal update functions which are later applied to build the journal,
now construct the journal directly (by modifying the parser state). This
is easier to understand and debug. It also removes any possibility of
the journal updates being a space leak. (They weren't, in fact memory
usage is now slightly higher, but that will be addressed in other ways.)

Also:

Journal data and journal parse info have been merged into one type (for
now), and field names are more consistent.

The ParsedJournal type alias has been added to distinguish being-parsed
and finalised journals.

Journal is now a monoid.

stats: fixed an issue with ordering of include files

journal: fixed an issue with ordering of included same-date transactions

timeclock: sessions can no longer span file boundaries (unclocked-out
sessions will be auto-closed at the end of the file).

expandPath now throws a proper IO error (and requires the IO monad).
2016-05-23 00:44:19 -07:00

125 lines
5.2 KiB
Haskell

{-# LANGUAGE CPP, FlexibleContexts, OverloadedStrings, QuasiQuotes, RecordWildCards #-}
-- | Add form data & handler. (The layout and js are defined in
-- Foundation so that the add form can be in the default layout for
-- all views.)
module Handler.AddForm where
import Import
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
import Data.Either (lefts,rights)
import Data.List (sort)
import qualified Data.List as L (head) -- qualified keeps dev & prod builds warning-free
import Data.Text (append, pack, unpack)
import qualified Data.Text as T
import Data.Time.Calendar
import Text.Parsec (digit, eof, many1, string, runParser)
import Hledger.Utils
import Hledger.Data hiding (num)
import Hledger.Read
import Hledger.Cli hiding (num)
-- Part of the data required from the add form.
-- Don't know how to handle the variable posting fields with yesod-form yet.
data AddForm = AddForm
{ addFormDate :: Day
, addFormDescription :: Maybe Text -- String
-- , addFormPostings :: [(AccountName, String)]
, addFormJournalFile :: Maybe Text -- FilePath
}
deriving Show
postAddForm :: Handler Html
postAddForm = do
let showErrors errs = do
-- error $ show errs -- XXX uncomment to prevent redirect for debugging
setMessage [shamlet|
Errors:<br>
$forall e<-errs
\#{e}<br>
|]
-- 1. process the fixed fields with yesod-form
VD{..} <- getViewData
let
validateJournalFile :: Text -> Either FormMessage Text
validateJournalFile f
| unpack f `elem` journalFilePaths j = Right f
| otherwise = Left $ MsgInvalidEntry $ pack "the selected journal file \"" `append` f `append` "\"is unknown"
validateDate :: Text -> Handler (Either FormMessage Day)
validateDate s = return $
case fixSmartDateStrEither' today $ strip $ unpack s of
Right d -> Right d
Left _ -> Left $ MsgInvalidEntry $ pack "could not parse date \"" `append` s `append` pack "\":" -- ++ show e)
formresult <- runInputPostResult $ AddForm
<$> ireq (checkMMap validateDate (pack . show) textField) "date"
<*> iopt textField "description"
<*> iopt (check validateJournalFile textField) "journal"
ok <- case formresult of
FormMissing -> showErrors ["there is no form data"::String] >> return False
FormFailure errs -> showErrors errs >> return False
FormSuccess dat -> do
let AddForm{
addFormDate =date
,addFormDescription=mdesc
,addFormJournalFile=mjournalfile
} = dat
desc = maybe "" unpack mdesc
journalfile = maybe (journalFilePath j) unpack mjournalfile
-- 2. the fixed fields look good; now process the posting fields adhocly,
-- getting either errors or a balanced transaction
(params,_) <- runRequestBody
let numberedParams s =
reverse $ dropWhile (T.null . snd) $ reverse $ sort
[ (n,v) | (k,v) <- params
, let en = parsewith (paramnamep s) $ T.unpack k
, isRight en
, let Right n = en
]
where paramnamep s = do {string s; n <- many1 digit; eof; return (read n :: Int)}
acctparams = numberedParams "account"
amtparams = numberedParams "amount"
num = length acctparams
paramErrs | num == 0 = ["at least one posting must be entered"]
| map fst acctparams == [1..num] &&
map fst amtparams `elem` [[1..num], [1..num-1]] = []
| otherwise = ["the posting parameters are malformed"]
eaccts = map (parsewith (accountnamep <* eof) . strip . T.unpack . snd) acctparams
eamts = map (runParser (amountp <* eof) mempty "" . strip . T.unpack . snd) amtparams
(accts, acctErrs) = (rights eaccts, map show $ lefts eaccts)
(amts', amtErrs) = (rights eamts, map show $ lefts eamts)
amts | length amts' == num = amts'
| otherwise = amts' ++ [missingamt]
errs = if not (null paramErrs) then paramErrs else (acctErrs ++ amtErrs)
etxn | not $ null errs = Left errs
| otherwise = either (\e -> Left [L.head $ lines e]) Right
(balanceTransaction Nothing $ nulltransaction {
tdate=date
,tdescription=desc
,tpostings=[nullposting{paccount=acct, pamount=Mixed [amt]} | (acct,amt) <- zip accts amts]
})
case etxn of
Left errs -> showErrors errs >> return False
Right t -> do
-- 3. all fields look good and form a balanced transaction; append it to the file
liftIO $ do ensureJournalFileExists journalfile
appendToJournalFileOrStdout journalfile $
showTransaction $
txnTieKnot -- XXX move into balanceTransaction
t
setMessage [shamlet|<span>Transaction added.|]
return True
if ok then redirect JournalR else redirect (JournalR, [("add","1")])