Empty final account/amount fields are now ignored. Empty fields followed by non-empty fields are not allowed.
		
			
				
	
	
		
			196 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			196 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
-- | POST helpers.
 | 
						|
 | 
						|
module Handler.Post where
 | 
						|
 | 
						|
import Import
 | 
						|
 | 
						|
import Control.Applicative
 | 
						|
import Data.Either (lefts,rights)
 | 
						|
import Data.List (intercalate, sort)
 | 
						|
import qualified Data.List as L (head) -- qualified keeps dev & prod builds warning-free
 | 
						|
import Data.Text (unpack)
 | 
						|
import qualified Data.Text as T
 | 
						|
import Text.Parsec (digit, eof, many1, string)
 | 
						|
import Text.Printf (printf)
 | 
						|
 | 
						|
import Hledger.Utils
 | 
						|
import Hledger.Data hiding (num)
 | 
						|
import Hledger.Read
 | 
						|
import Hledger.Cli hiding (num)
 | 
						|
 | 
						|
 | 
						|
-- | Handle a post from any of the edit forms.
 | 
						|
handlePost :: Handler Html
 | 
						|
handlePost = do
 | 
						|
  action <- lookupPostParam  "action"
 | 
						|
  case action of Just "add"    -> handleAdd
 | 
						|
                 Just "edit"   -> handleEdit
 | 
						|
                 Just "import" -> handleImport
 | 
						|
                 _             -> invalidArgs ["invalid action"]
 | 
						|
 | 
						|
-- | Handle a post from the transaction add form.
 | 
						|
handleAdd :: Handler Html
 | 
						|
handleAdd = do
 | 
						|
  VD{..} <- getViewData
 | 
						|
  -- gruesome adhoc form handling, port to yesod-form later
 | 
						|
  mjournal <- lookupPostParam  "journal"
 | 
						|
  mdate <- lookupPostParam  "date"
 | 
						|
  mdesc <- lookupPostParam  "description"
 | 
						|
  let edate = maybe (Left "date required") (either (\e -> Left $ showDateParseError e) Right . fixSmartDateStrEither today . strip . unpack) mdate
 | 
						|
      edesc = Right $ maybe "" unpack mdesc
 | 
						|
      ejournal = maybe (Right $ journalFilePath j)
 | 
						|
                       (\f -> let f' = unpack f in
 | 
						|
                              if f' `elem` journalFilePaths j
 | 
						|
                              then Right f'
 | 
						|
                              else Left $ "unrecognised journal file path: " ++ f'
 | 
						|
                              )
 | 
						|
                       mjournal
 | 
						|
      estrs = [edate, edesc, ejournal]
 | 
						|
      (errs1, [date,desc,journalpath]) = (lefts estrs, rights estrs) -- XXX irrefutable
 | 
						|
  (params,_) <- runRequestBody
 | 
						|
  -- mtrace params
 | 
						|
  let paramnamep s = do {string s; n <- many1 digit; eof; return (read n :: Int)}
 | 
						|
      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
 | 
						|
                ]
 | 
						|
      acctparams = numberedParams "account"
 | 
						|
      amtparams  = numberedParams "amount"
 | 
						|
      num = length acctparams
 | 
						|
      paramErrs | map fst acctparams == [1..num] &&
 | 
						|
                  map fst amtparams `elem` [[1..num], [1..num-1]] = []
 | 
						|
                | otherwise = ["malformed account/amount parameters"]
 | 
						|
      eaccts = map (parsewith (accountnamep <* eof) . strip . T.unpack . snd) acctparams
 | 
						|
      eamts  = map (parseWithCtx nullctx (amountp <* eof) . 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]
 | 
						|
      -- if no errors so far, generate a transaction and balance it or get the error.
 | 
						|
      errs = errs1 ++ if not (null paramErrs) then paramErrs else (acctErrs ++ amtErrs)
 | 
						|
      et | not $ null errs = Left errs
 | 
						|
         | otherwise = either (\e -> Left ["unbalanced postings: " ++ (L.head $ lines e)]) Right
 | 
						|
                        (balanceTransaction Nothing $ nulltransaction {
 | 
						|
                            tdate=parsedate date
 | 
						|
                           ,tdescription=desc
 | 
						|
                           ,tpostings=[nullposting{paccount=acct, pamount=Mixed [amt]} | (acct,amt) <- zip accts amts]
 | 
						|
                           })
 | 
						|
  -- display errors or add transaction
 | 
						|
  case et of
 | 
						|
   Left errs' -> do
 | 
						|
    error $ show errs' -- XXX
 | 
						|
    -- save current form values in session
 | 
						|
    -- setMessage $ toHtml $ intercalate "; " errs
 | 
						|
    setMessage [shamlet|
 | 
						|
                 Errors:<br>
 | 
						|
                 $forall e<-errs'
 | 
						|
                  \#{e}<br>
 | 
						|
               |]
 | 
						|
   Right t -> do
 | 
						|
    let t' = txnTieKnot t -- XXX move into balanceTransaction
 | 
						|
    liftIO $ do ensureJournalFileExists journalpath
 | 
						|
                appendToJournalFileOrStdout journalpath $ showTransaction t'
 | 
						|
    setMessage [shamlet|<span>Transaction added.|]
 | 
						|
 | 
						|
  redirect (JournalR) -- , [("add","1")])
 | 
						|
 | 
						|
-- personForm :: Html -> MForm Handler (FormResult Person, Widget)
 | 
						|
-- personForm extra = do
 | 
						|
--     (nameRes, nameView) <- mreq textField "this is not used" Nothing
 | 
						|
--     (ageRes, ageView) <- mreq intField "neither is this" Nothing
 | 
						|
--     let personRes = Person <$> nameRes <*> ageRes
 | 
						|
--     let widget = do
 | 
						|
--             toWidget
 | 
						|
--                 [lucius|
 | 
						|
--                     ##{fvId ageView} {
 | 
						|
--                         width: 3em;
 | 
						|
--                     }
 | 
						|
--                 |]
 | 
						|
--             [whamlet|
 | 
						|
--                 #{extra}
 | 
						|
--                 <p>
 | 
						|
--                     Hello, my name is #
 | 
						|
--                     ^{fvInput nameView}
 | 
						|
--                     \ and I am #
 | 
						|
--                     ^{fvInput ageView}
 | 
						|
--                     \ years old. #
 | 
						|
--                     <input type=submit value="Introduce myself">
 | 
						|
--             |]
 | 
						|
--     return (personRes, widget)
 | 
						|
--
 | 
						|
--     ((res, widget), enctype) <- runFormGet personForm
 | 
						|
--     defaultLayout
 | 
						|
--         [whamlet|
 | 
						|
--             <p>Result: #{show res}
 | 
						|
--             <form enctype=#{enctype}>
 | 
						|
--                 ^{widget}
 | 
						|
--         |]
 | 
						|
 | 
						|
-- | Handle a post from the journal edit form.
 | 
						|
handleEdit :: Handler Html
 | 
						|
handleEdit = do
 | 
						|
  VD{..} <- getViewData
 | 
						|
  -- get form input values, or validation errors.
 | 
						|
  -- getRequest >>= liftIO (reqRequestBody req) >>= mtrace
 | 
						|
  mtext <- lookupPostParam "text"
 | 
						|
  mjournal <- lookupPostParam "journal"
 | 
						|
  let etext = maybe (Left "No value provided") (Right . unpack) mtext
 | 
						|
      ejournal = maybe (Right $ journalFilePath j)
 | 
						|
                       (\f -> let f' = unpack f in
 | 
						|
                              if f' `elem` journalFilePaths j
 | 
						|
                              then Right f'
 | 
						|
                              else Left "unrecognised journal file path")
 | 
						|
                       mjournal
 | 
						|
      estrs = [etext, ejournal]
 | 
						|
      errs = lefts estrs
 | 
						|
      [text,journalpath] = rights estrs
 | 
						|
  -- display errors or perform edit
 | 
						|
  if not $ null errs
 | 
						|
   then do
 | 
						|
    setMessage $ toHtml (intercalate "; " errs :: String)
 | 
						|
    redirect JournalR
 | 
						|
 | 
						|
   else do
 | 
						|
    -- try to avoid unnecessary backups or saving invalid data
 | 
						|
    filechanged' <- liftIO $ journalSpecifiedFileIsNewer j journalpath
 | 
						|
    told <- liftIO $ readFileStrictly journalpath
 | 
						|
    let tnew = filter (/= '\r') text
 | 
						|
        changed = tnew /= told || filechanged'
 | 
						|
    if not changed
 | 
						|
     then do
 | 
						|
       setMessage "No change"
 | 
						|
       redirect JournalR
 | 
						|
     else do
 | 
						|
      jE <- liftIO $ readJournal Nothing Nothing True (Just journalpath) tnew
 | 
						|
      either
 | 
						|
       (\e -> do
 | 
						|
          setMessage $ toHtml e
 | 
						|
          redirect JournalR)
 | 
						|
       (const $ do
 | 
						|
          liftIO $ writeFileWithBackup journalpath tnew
 | 
						|
          setMessage $ toHtml (printf "Saved journal %s\n" (show journalpath) :: String)
 | 
						|
          redirect JournalR)
 | 
						|
       jE
 | 
						|
 | 
						|
-- | Handle a post from the journal import form.
 | 
						|
handleImport :: Handler Html
 | 
						|
handleImport = do
 | 
						|
  setMessage "can't handle file upload yet"
 | 
						|
  redirect JournalR
 | 
						|
  -- -- get form input values, or basic validation errors. E means an Either value.
 | 
						|
  -- fileM <- runFormPost $ maybeFileInput "file"
 | 
						|
  -- let fileE = maybe (Left "No file provided") Right fileM
 | 
						|
  -- -- display errors or import transactions
 | 
						|
  -- case fileE of
 | 
						|
  --  Left errs -> do
 | 
						|
  --   setMessage errs
 | 
						|
  --   redirect JournalR
 | 
						|
 | 
						|
  --  Right s -> do
 | 
						|
  --    setMessage s
 | 
						|
  --    redirect JournalR
 | 
						|
 |