web: improve add form validation (#223, #234)

The add form is now partly using yesod-form.
This commit is contained in:
Simon Michael 2015-02-16 18:22:17 +00:00
parent 16aaf35c4b
commit d3453c66c1
2 changed files with 92 additions and 68 deletions

View File

@ -1,4 +1,6 @@
-- | POST helpers. -- | 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 module Handler.AddForm where
@ -8,10 +10,12 @@ import Control.Applicative
import Data.Either (lefts,rights) import Data.Either (lefts,rights)
import Data.List (sort) import Data.List (sort)
import qualified Data.List as L (head) -- qualified keeps dev & prod builds warning-free import qualified Data.List as L (head) -- qualified keeps dev & prod builds warning-free
-- import Data.Maybe
import Data.Text (unpack) import Data.Text (unpack)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time.Calendar -- import Data.Time.Calendar
import Text.Parsec (digit, eof, many1, string, runParser) import Text.Parsec (digit, eof, many1, string, runParser)
-- import Yesod.Form.Jquery
import Hledger.Utils import Hledger.Utils
import Hledger.Data hiding (num) import Hledger.Data hiding (num)
@ -19,74 +23,92 @@ import Hledger.Read
import Hledger.Cli hiding (num) import Hledger.Cli hiding (num)
-- | Handle a post from the transaction add form. -- 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
{ addFormJournalFile :: Maybe Text -- FilePath
, addFormDate :: Maybe Text -- Day
, addFormDescription :: Maybe Text -- String
-- , addFormPostings :: [(AccountName, String)]
}
deriving Show
postAddForm :: Handler Html postAddForm :: Handler Html
postAddForm = do postAddForm = do
VD{..} <- getViewData VD{..} <- getViewData
-- XXX gruesome form handling, port to yesod-form. cf #234 let showErrors errs = do
mjournalpath <- lookupPostParam "journal" -- error $ show errs -- XXX uncomment to prevent redirect, for debugging
mdate <- lookupPostParam "date" setMessage [shamlet|
mdesc <- lookupPostParam "description" Error:<br>
let edate = maybe (Left "date required") (either (\e -> Left $ showDateParseError e) Right . fixSmartDateStrEither today . strip . unpack) mdate $forall e<-errs
edesc = Right $ maybe "" unpack mdesc \#{e}<br>
ejournalpath = maybe |]
(Right $ journalFilePath j) formresult <- runInputPostResult $ AddForm
(\f -> let f' = unpack f in <$> iopt textField "journal"
if f' `elem` journalFilePaths j <*> iopt textField "date"
then Right f' -- (jqueryDayField def
else Left $ "unrecognised journal file path: " ++ f' -- {
) -- jdsChangeYear = True -- give a year dropdown
mjournalpath -- , jdsYearRange = "1900:-5" -- 1900 till five years ago
estrs = [edate, edesc, ejournalpath] -- }) "date"
(errs1, [date, desc, journalpath]) = case (lefts estrs, rights estrs) of <*> iopt textField "description"
([], [_,_,_]) -> ([], rights estrs) case formresult of
_ -> (lefts estrs, [error "",error "",error ""]) -- RHS won't be used FormMissing -> showErrors ["there is no form data"::String]
(params,_) <- runRequestBody FormFailure errs -> showErrors errs
-- mtrace params FormSuccess formdata -> do
let paramnamep s = do {string s; n <- many1 digit; eof; return (read n :: Int)} let AddForm{
numberedParams s = addFormJournalFile=mjournalfile
reverse $ dropWhile (T.null . snd) $ reverse $ sort ,addFormDate =mdate
[ (n,v) | (k,v) <- params ,addFormDescription=mdesc
, let en = parsewith (paramnamep s) $ T.unpack k } = formdata
, isRight en date = parsedate $ fixSmartDateStr today $ maybe "today" (strip . unpack) mdate
, let Right n = en desc = maybe "" unpack mdesc
] journalfile = maybe
acctparams = numberedParams "account" (journalFilePath j)
amtparams = numberedParams "amount" (\f' -> let f = unpack f' in
num = length acctparams if f `elem` journalFilePaths j
paramErrs | map fst acctparams == [1..num] && then f
map fst amtparams `elem` [[1..num], [1..num-1]] = [] else error $ "the selected journal file is unknown: " ++ f)
| otherwise = ["malformed account/amount parameters"] mjournalfile
eaccts = map (parsewith (accountnamep <* eof) . strip . T.unpack . snd) acctparams
eamts = map (runParser (amountp <* eof) nullctx "" . strip . T.unpack . snd) amtparams (params,_) <- runRequestBody
(accts, acctErrs) = (rights eaccts, map show $ lefts eaccts) let numberedParams s =
(amts', amtErrs) = (rights eamts, map show $ lefts eamts) reverse $ dropWhile (T.null . snd) $ reverse $ sort
amts | length amts' == num = amts' [ (n,v) | (k,v) <- params
| otherwise = amts' ++ [missingamt] , let en = parsewith (paramnamep s) $ T.unpack k
-- if no errors so far, generate a transaction and balance it or get the error. , isRight en
errs = errs1 ++ if not (null paramErrs) then paramErrs else (acctErrs ++ amtErrs) , let Right n = en
et | not $ null errs = Left errs ]
| otherwise = either (\e -> Left ["unbalanced postings: " ++ (L.head $ lines e)]) Right where paramnamep s = do {string s; n <- many1 digit; eof; return (read n :: Int)}
(balanceTransaction Nothing $ nulltransaction { acctparams = numberedParams "account"
tdate=parsedate date amtparams = numberedParams "amount"
,tdescription=desc num = length acctparams
,tpostings=[nullposting{paccount=acct, pamount=Mixed [amt]} | (acct,amt) <- zip accts amts] paramErrs | num == 0 = ["at least one posting must be entered"]
}) | map fst acctparams == [1..num] &&
-- display errors or add transaction map fst amtparams `elem` [[1..num], [1..num-1]] = []
case et of | otherwise = ["the posting parameters are malformed"]
Left errs' -> do eaccts = map (parsewith (accountnamep <* eof) . strip . T.unpack . snd) acctparams
error $ show errs' -- XXX eamts = map (runParser (amountp <* eof) nullctx "" . strip . T.unpack . snd) amtparams
-- save current form values in session (accts, acctErrs) = (rights eaccts, map show $ lefts eaccts)
-- setMessage $ toHtml $ intercalate "; " errs (amts', amtErrs) = (rights eamts, map show $ lefts eamts)
setMessage [shamlet| amts | length amts' == num = amts'
Errors:<br> | otherwise = amts' ++ [missingamt]
$forall e<-errs' -- if no errors so far, generate a transaction and balance it or get the error.
\#{e}<br> errs = if not (null paramErrs) then paramErrs else (acctErrs ++ amtErrs)
|] et | not $ null errs = Left errs
Right t -> do | otherwise = either (\e -> Left [L.head $ lines e]) Right
let t' = txnTieKnot t -- XXX move into balanceTransaction (balanceTransaction Nothing $ nulltransaction {
liftIO $ do ensureJournalFileExists journalpath tdate=date
appendToJournalFileOrStdout journalpath $ showTransaction t' ,tdescription=desc
setMessage [shamlet|<span>Transaction added.|] ,tpostings=[nullposting{paccount=acct, pamount=Mixed [amt]} | (acct,amt) <- zip accts amts]
})
-- display errors or add transaction
case et of
Left errs -> showErrors errs
Right t -> do
let t' = txnTieKnot t -- XXX move into balanceTransaction
liftIO $ do ensureJournalFileExists journalfile
appendToJournalFileOrStdout journalfile $ showTransaction t'
setMessage [shamlet|<span>Transaction added.|]
redirect (JournalR) -- , [("add","1")]) redirect (JournalR) -- , [("add","1")])

View File

@ -187,6 +187,7 @@ library
, yaml , yaml
, yesod >= 1.4 && < 1.5 , yesod >= 1.4 && < 1.5
, yesod-core , yesod-core
, yesod-form
, yesod-static , yesod-static
, json , json
-- required by extra ghci utilities: -- required by extra ghci utilities:
@ -258,6 +259,7 @@ executable hledger-web
, yaml , yaml
, yesod >= 1.4 && < 1.5 , yesod >= 1.4 && < 1.5
, yesod-core , yesod-core
, yesod-form
, yesod-static , yesod-static
, json , json
-- required by extra ghci utilities: -- required by extra ghci utilities: