fix: web: add form: re-enable the file field (#1229)
It was ignored since cc1241fa2 in 2018. Also simplify some types.
			
			
This commit is contained in:
		
							parent
							
								
									51098d9150
								
							
						
					
					
						commit
						d718f2c50c
					
				| @ -35,12 +35,11 @@ postAddR = do | |||||||
| 
 | 
 | ||||||
|   ((res, view), enctype) <- runFormPost $ addForm j today |   ((res, view), enctype) <- runFormPost $ addForm j today | ||||||
|   case res of |   case res of | ||||||
|     FormSuccess res' -> do |     FormSuccess (t,f) -> do | ||||||
|       let t = txnTieKnot res' |       let t' = txnTieKnot t | ||||||
|       -- XXX(?) move into balanceTransaction |       liftIO $ do | ||||||
|       liftIO $ ensureJournalFileExists (journalFilePath j) |         ensureJournalFileExists f | ||||||
|       -- XXX why not journalAddTransaction ? |         appendToJournalFileOrStdout f (showTransaction t') | ||||||
|       liftIO $ appendToJournalFileOrStdout (journalFilePath j) (showTransaction t) |  | ||||||
|       setMessage "Transaction added." |       setMessage "Transaction added." | ||||||
|       redirect JournalR |       redirect JournalR | ||||||
|     FormMissing -> showForm view enctype |     FormMissing -> showForm view enctype | ||||||
|  | |||||||
| @ -25,19 +25,13 @@ import Text.Megaparsec (bundleErrors, eof, parseErrorTextPretty, runParser) | |||||||
| import Yesod | import Yesod | ||||||
| 
 | 
 | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Web.Foundation -- (App, Form, Handler, Widget) | import Hledger.Web.Foundation (App, Handler, Widget) | ||||||
| import Hledger.Web.Settings (widgetFile) | import Hledger.Web.Settings (widgetFile) | ||||||
|  | import Data.Function ((&)) | ||||||
| 
 | 
 | ||||||
| -- addModal :: |  | ||||||
| --      ( MonadWidget m |  | ||||||
| --      , r ~ Route (HandlerSite m) |  | ||||||
| --      , m ~ WidgetFor (HandlerSite m) |  | ||||||
| --      , RenderMessage (HandlerSite m) FormMessage |  | ||||||
| --      ) |  | ||||||
| --   => r -> Journal -> Day -> m () |  | ||||||
| addModal :: Route App -> Journal -> Day -> Widget | addModal :: Route App -> Journal -> Day -> Widget | ||||||
| addModal addR j today = do | addModal addR j today = do | ||||||
|   (addView, addEnctype) <- generateFormPost (addForm j today) |   (addView, addEnctype) <- handlerToWidget $ generateFormPost (addForm j today) | ||||||
|   [whamlet| |   [whamlet| | ||||||
| <div .modal #addmodal tabindex="-1" role="dialog" aria-labelledby="addLabel" aria-hidden="true"> | <div .modal #addmodal tabindex="-1" role="dialog" aria-labelledby="addLabel" aria-hidden="true"> | ||||||
|   <div .modal-dialog .modal-lg> |   <div .modal-dialog .modal-lg> | ||||||
| @ -50,113 +44,88 @@ addModal addR j today = do | |||||||
|           ^{addView} |           ^{addView} | ||||||
| |] | |] | ||||||
| 
 | 
 | ||||||
| -- addForm :: | addForm :: Journal -> Day -> Markup -> MForm Handler (FormResult (Transaction,FilePath), Widget) | ||||||
| --      (site ~ HandlerSite m, RenderMessage site FormMessage, MonadHandler m) |  | ||||||
| --   => Journal |  | ||||||
| --   -> Day |  | ||||||
| --   -> Markup |  | ||||||
| --   -> MForm m (FormResult Transaction, WidgetFor site ()) |  | ||||||
| addForm :: |  | ||||||
|   (MonadHandler m, RenderMessage (HandlerSite m) FormMessage) =>  |  | ||||||
|   Journal -> Day -> Markup -> |  | ||||||
|   MForm m (FormResult Transaction, WidgetFor (HandlerSite m) ()) |  | ||||||
| addForm j today = identifyForm "add" $ \extra -> do | addForm j today = identifyForm "add" $ \extra -> do | ||||||
|   let  -- bindings used in add-form.hamlet |   let  -- bindings used in add-form.hamlet | ||||||
|     descriptions = foldMap S.fromList [journalPayeesDeclaredOrUsed j, journalDescriptions j] |     descriptions = foldMap S.fromList [journalPayeesDeclaredOrUsed j, journalDescriptions j] | ||||||
|     files = fst <$> jfiles j |     files = fst <$> jfiles j | ||||||
|   (dateRes, dateView) <- mreq dateField dateSettings Nothing |   (dateRes, dateView) <- mreq dateField dateSettings Nothing | ||||||
|   (descRes, descView) <- mreq textField descSettings Nothing |   (descRes, descView) <- mreq textField descSettings Nothing | ||||||
|   (acctRes, _)        <- mreq listField "account" Nothing |   (acctsRes, _)       <- mreq listField acctSettings Nothing | ||||||
|   (amtRes , _)        <- mreq listField "amount" Nothing |   (amtsRes, _)        <- mreq listField amtSettings  Nothing | ||||||
|   let (postRes, displayRows) = validatePostings acctRes amtRes |   (fileRes, fileView) <- mreq fileField' fileSettings Nothing | ||||||
|   pure (validateTransaction dateRes descRes postRes, $(widgetFile "add-form")) |   let | ||||||
| 
 |     (postingsRes, displayRows) = validatePostings acctsRes amtsRes | ||||||
|  |     formRes = validateTransaction dateRes descRes postingsRes fileRes | ||||||
|  |   return (formRes, $(widgetFile "add-form")) | ||||||
|   where |   where | ||||||
|     -- field settings |     -- custom fields | ||||||
|     dateSettings = FieldSettings "date" Nothing Nothing (Just "date") |     dateField = textField & checkMMap (pure . validateDate) (T.pack . show) | ||||||
|       [("class", "form-control input-lg"), ("placeholder", "Date")] |  | ||||||
|     descSettings = FieldSettings "desc" Nothing Nothing (Just "description") |  | ||||||
|       [("class", "form-control input-lg typeahead"), ("placeholder", "Description"), ("size", "40")] |  | ||||||
| 
 |  | ||||||
|     -- custom field types |  | ||||||
|     dateField = checkMMap (pure . validateDate) (T.pack . show) textField |  | ||||||
|       where |       where | ||||||
|         validateDate s = |         validateDate s = | ||||||
|           first (const ("Invalid date format" :: Text)) $ |           first (const ("Invalid date format" :: Text)) $ | ||||||
|           fixSmartDateStrEither' today (T.strip s) |           fixSmartDateStrEither' today (T.strip s) | ||||||
|     listField = Field |     listField = Field | ||||||
|       { fieldParse = const . pure . Right . Just . dropWhileEnd T.null |       { fieldParse = const . pure . Right . Just . dropWhileEnd T.null | ||||||
|       , fieldView = error "Don't render using this!"  -- PARTIAL: |       , fieldView = error' "listField should not be used for rendering"  -- PARTIAL: | ||||||
|       , fieldEnctype = UrlEncoded |       , fieldEnctype = UrlEncoded | ||||||
|       } |       } | ||||||
| 
 |     fileField' :: Field Handler FilePath | ||||||
|     -- helpers used in add-form.hamlet |     fileField' = selectFieldList [(T.pack f, f) | f <- fs] & check validateFilepath | ||||||
|     toBloodhoundJson :: [Text] -> Markup |  | ||||||
|     toBloodhoundJson ts = |  | ||||||
|       -- This used to work, but since 1.16, it seems like something changed. |  | ||||||
|       -- toJSON ("a"::Text) gives String "a" instead of "a", etc. |  | ||||||
|       -- preEscapedString . escapeJSSpecialChars . show . toJSON |  | ||||||
| 
 |  | ||||||
|       preEscapedText $ T.concat [ |  | ||||||
|         "[", |  | ||||||
|         T.intercalate "," $ map ( |  | ||||||
|           ("{\"value\":" <>). |  | ||||||
|           (<> "}"). |  | ||||||
|           -- This will convert a value such as ``hledger!`` into |  | ||||||
|           -- ``atob("aGxlZGdlciE=")``. When this gets evaluated on the client, |  | ||||||
|           -- the resulting string is ``hledger!`` again. The same data is |  | ||||||
|           -- passed, but the user-controlled bit of that string can only use |  | ||||||
|           -- characters [a-zA-Z0-9+=/], making it impossible to break out of |  | ||||||
|           -- string context. |  | ||||||
|           b64wrap |  | ||||||
|           ) ts, |  | ||||||
|         "]" |  | ||||||
|         ] |  | ||||||
|       where |       where | ||||||
|         b64wrap = ("atob(\""<>) . (<>"\")") . encodeBase64 |         fs = journalFilePaths j | ||||||
|  |         validateFilepath :: FilePath -> Either FormMessage FilePath | ||||||
|  |         validateFilepath f | ||||||
|  |           | f `elem` fs = Right f | ||||||
|  |           | otherwise = Left $ MsgInputNotFound $ T.pack f | ||||||
|  |     -- field settings | ||||||
|  |     dateSettings = FieldSettings "date" Nothing Nothing (Just "date") [("class", "form-control input-lg"), ("placeholder", "Date")] | ||||||
|  |     descSettings = FieldSettings "desc" Nothing Nothing (Just "description") [("class", "form-control input-lg typeahead"), ("placeholder", "Description"), ("size", "40")] | ||||||
|  |     acctSettings = FieldSettings "account" Nothing Nothing (Just "account") [] | ||||||
|  |     amtSettings  = FieldSettings "amount" Nothing Nothing (Just "amount") [] | ||||||
|  |     fileSettings = FieldSettings "file" Nothing Nothing (Just "file") [("class", "form-control input-lg")] | ||||||
| 
 | 
 | ||||||
| validateTransaction :: | validateTransaction :: | ||||||
|      FormResult Day |      FormResult Day -> FormResult Text -> FormResult [Posting] -> FormResult FilePath | ||||||
|   -> FormResult Text |   -> FormResult (Transaction, FilePath) | ||||||
|   -> FormResult [Posting] | validateTransaction dateRes descRes postingsRes fileRes = | ||||||
|   -> FormResult Transaction |   case makeTransaction <$> dateRes <*> descRes <*> postingsRes <*> fileRes of | ||||||
| validateTransaction dateRes descRes postingsRes = |     FormSuccess (txn,f) -> case balanceTransaction defbalancingopts txn of | ||||||
|   case makeTransaction <$> dateRes <*> descRes <*> postingsRes of |       Left e     -> FormFailure [T.pack e] | ||||||
|     FormSuccess txn -> case balanceTransaction defbalancingopts txn of |       Right txn' -> FormSuccess (txn',f) | ||||||
|       Left e -> FormFailure [T.pack e] |  | ||||||
|       Right txn' -> FormSuccess txn' |  | ||||||
|     x -> x |     x -> x | ||||||
|   where |   where | ||||||
|     makeTransaction date desc postings = |     makeTransaction date desc postings f = | ||||||
|       nulltransaction { |       (nulltransaction { | ||||||
|          tdate        = date |          tdate = date | ||||||
|         ,tdescription = desc |         ,tdescription = desc | ||||||
|         ,tpostings    = postings |         ,tpostings = postings | ||||||
|         } |         ,tsourcepos = (initialPos f, initialPos f) | ||||||
|  |         }, f) | ||||||
| 
 | 
 | ||||||
| -- | Parse a list of postings out of a list of accounts and a corresponding list | -- | Parse a list of postings out of a list of accounts and a corresponding list | ||||||
| -- of amounts | -- of amounts | ||||||
| validatePostings :: | validatePostings :: | ||||||
|      FormResult [Text] |      FormResult [Text] -> FormResult [Text] | ||||||
|   -> FormResult [Text] |  | ||||||
|   -> (FormResult [Posting], [(Int, (Text, Text, Maybe Text, Maybe Text))]) |   -> (FormResult [Posting], [(Int, (Text, Text, Maybe Text, Maybe Text))]) | ||||||
| validatePostings acctRes amtRes = let | validatePostings acctsRes amtsRes = let | ||||||
| 
 | 
 | ||||||
|   -- Zip accounts and amounts, fill in missing values and drop empty rows. |   -- Zip accounts and amounts, fill in missing values and drop empty rows. | ||||||
|   rows :: [(Text, Text)] |   rows :: [(Text, Text)] | ||||||
|   rows = filter (/= ("", "")) $ zipDefault "" (formSuccess [] acctRes) (formSuccess [] amtRes) |   rows = filter (/= ("", "")) $ zipDefault "" (formSuccess [] acctsRes) (formSuccess [] amtsRes) | ||||||
| 
 | 
 | ||||||
|   -- Parse values and check for incomplete rows with only an account or an amount. |   -- 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 |   -- The boolean in unfoldr state is for special handling of 'missingamt', where | ||||||
|   -- one row may have only an account and not an amount. |   -- one row may have only an account and not an amount. | ||||||
|   postings :: [(Text, Text, Either (Maybe Text, Maybe Text) Posting)] |   postings :: [(Text, Text, Either (Maybe Text, Maybe Text) Posting)] | ||||||
|   postings = unfoldr go (True, rows) |   postings = unfoldr go (True, rows) | ||||||
|   go (True, (x, ""):y:xs) = Just ((x, "", zipRow (checkAccount x) (Left "Missing amount")), (True, y:xs)) |     where | ||||||
|   go (True, (x, ""):xs) = Just ((x, "", zipRow (checkAccount x) (Right missingamt)), (False, xs)) |       go (True, (x, ""):y:xs) = Just ((x, "", zipRow (checkAccount x) (Left "Missing amount")), (True, y:xs)) | ||||||
|   go (False, (x, ""):xs) = Just ((x, "", zipRow (checkAccount x) (Left "Missing amount")), (False, xs)) |       go (True, (x, ""):xs) = Just ((x, "", zipRow (checkAccount x) (Right missingamt)), (False, xs)) | ||||||
|   go (_, ("", y):xs) = Just (("", y, zipRow (Left "Missing account") (checkAmount y)), (False, xs)) |       go (False, (x, ""):xs) = Just ((x, "", zipRow (checkAccount x) (Left "Missing amount")), (False, xs)) | ||||||
|   go (_, (x, y):xs) = Just ((x, y, zipRow (checkAccount x) (checkAmount y)), (True, xs)) |       go (_, ("", y):xs) = Just (("", y, zipRow (Left "Missing account") (checkAmount y)), (False, xs)) | ||||||
|   go (_, []) = Nothing |       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) (Left e') = Left (Just e, Just e') | ||||||
|   zipRow (Left e) (Right _) = Left (Just e, Nothing) |   zipRow (Left e) (Right _) = Left (Just e, Nothing) | ||||||
| @ -174,7 +143,7 @@ validatePostings acctRes amtRes = let | |||||||
| 
 | 
 | ||||||
|   -- Add errors to forms with zero rows if the form is not a FormMissing |   -- Add errors to forms with zero rows if the form is not a FormMissing | ||||||
|   result :: [(Text, Text, Either (Maybe Text, Maybe Text) Posting)] |   result :: [(Text, Text, Either (Maybe Text, Maybe Text) Posting)] | ||||||
|   result = case (acctRes, amtRes) of |   result = case (acctsRes, amtsRes) of | ||||||
|     (FormMissing, FormMissing) -> postings |     (FormMissing, FormMissing) -> postings | ||||||
|     _ -> case postings of |     _ -> case postings of | ||||||
|       [] -> [ ("", "", Left (Just "Missing account", Just "Missing amount")) |       [] -> [ ("", "", Left (Just "Missing account", Just "Missing amount")) | ||||||
| @ -196,6 +165,30 @@ validatePostings acctRes amtRes = let | |||||||
| 
 | 
 | ||||||
|   in (formResult, zip [(1 :: Int)..] display) |   in (formResult, zip [(1 :: Int)..] display) | ||||||
| 
 | 
 | ||||||
|  | -- helper for add-form.hamlet | ||||||
|  | toBloodhoundJson :: [Text] -> Markup | ||||||
|  | toBloodhoundJson ts = | ||||||
|  |   -- This used to work, but since 1.16, it seems like something changed. | ||||||
|  |   -- toJSON ("a"::Text) gives String "a" instead of "a", etc. | ||||||
|  |   -- preEscapedString . escapeJSSpecialChars . show . toJSON | ||||||
|  | 
 | ||||||
|  |   preEscapedText $ T.concat [ | ||||||
|  |     "[", | ||||||
|  |     T.intercalate "," $ map ( | ||||||
|  |       ("{\"value\":" <>). | ||||||
|  |       (<> "}"). | ||||||
|  |       -- This will convert a value such as ``hledger!`` into | ||||||
|  |       -- ``atob("aGxlZGdlciE=")``. When this gets evaluated on the client, | ||||||
|  |       -- the resulting string is ``hledger!`` again. The same data is | ||||||
|  |       -- passed, but the user-controlled bit of that string can only use | ||||||
|  |       -- characters [a-zA-Z0-9+=/], making it impossible to break out of | ||||||
|  |       -- string context. | ||||||
|  |       b64wrap | ||||||
|  |       ) ts, | ||||||
|  |     "]" | ||||||
|  |     ] | ||||||
|  |   where | ||||||
|  |     b64wrap = ("atob(\""<>) . (<>"\")") . encodeBase64 | ||||||
| 
 | 
 | ||||||
| zipDefault :: a -> [a] -> [a] -> [(a, a)] | zipDefault :: a -> [a] -> [a] -> [(a, a)] | ||||||
| zipDefault def (b:bs) (c:cs) = (b, c):(zipDefault def bs cs) | zipDefault def (b:bs) (c:cs) = (b, c):(zipDefault def bs cs) | ||||||
|  | |||||||
| @ -60,8 +60,7 @@ | |||||||
| 
 | 
 | ||||||
| $if length files > 1 | $if length files > 1 | ||||||
|   <br> |   <br> | ||||||
|   <span .input-lg>to: |   <div style="display:inline-block; width:auto;" .form-group :isJust (fvErrors fileView):.has-error> | ||||||
|     <select #journalselect .form-control.input-lg name=file style="width:auto; display:inline-block;"> |     To file: ^{fvInput fileView} | ||||||
|       $forall f <- files |     $maybe err <- fvErrors fileView | ||||||
|         <option value=#{f}>#{f} |       <span .help-block .error-block>#{err} | ||||||
| <span .small style="padding-left:2em;"> |  | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user