webyesod: a destructive wiki-style edit form for journals
Warning: this is the first hledger feature which can alter your existing journal data. You can now edit, or ERASE, the (top-level) journal file through the web ui, and there is no access control. A numbered backup of the file will be saved at each edit, in normal circumstances.
This commit is contained in:
		
							parent
							
								
									b462eea9de
								
							
						
					
					
						commit
						aa21f95b9e
					
				| @ -79,6 +79,7 @@ mkYesod "HledgerWebApp" [$parseRoutes| | ||||
| /             IndexPage        GET | ||||
| /style.css    StyleCss         GET | ||||
| /journal      JournalPage      GET POST | ||||
| /edit         EditPage         GET POST | ||||
| /register     RegisterPage     GET | ||||
| /balance      BalancePage      GET | ||||
| |] | ||||
| @ -164,12 +165,14 @@ navbar here a p = [$hamlet| | ||||
| navlinks :: HledgerWebAppRoutes -> String -> String -> Hamlet HledgerWebAppRoutes | ||||
| navlinks here a p = [$hamlet| | ||||
|  #navlinks | ||||
|   ^journallink^ | $ | ||||
|   ^registerlink^ | $ | ||||
|   ^balancelink^ | ||||
|   ^journallink^ $ | ||||
|   (^editlink^) $ | ||||
|   | ^registerlink^ $ | ||||
|   | ^balancelink^ $ | ||||
| |] | ||||
|  where | ||||
|   journallink = navlink here "journal" JournalPage | ||||
|   editlink = navlink here "edit" EditPage | ||||
|   registerlink = navlink here "register" RegisterPage | ||||
|   balancelink = navlink here "balance" BalancePage | ||||
|   navlink here s dest = [$hamlet|%a.$style$!href=@?u@ $string.s$|] | ||||
| @ -203,10 +206,9 @@ helplink topic label = [$hamlet|%a!href=$string.u$ $string.label$|] | ||||
| addform :: Hamlet HledgerWebAppRoutes | ||||
| addform = [$hamlet| | ||||
|  %form!method=POST | ||||
|   %table#addform!cellpadding=0!cellspacing=0!!border=0 | ||||
|   %table.form#addform!cellpadding=0!cellspacing=0!!border=0 | ||||
|    %tr.formheading | ||||
|     %td!colspan=4 | ||||
|      %span!style=float:right; ^formhelp^ | ||||
|      %span#formheading Add a transaction: | ||||
|    %tr | ||||
|     %td!colspan=4 | ||||
| @ -332,3 +334,93 @@ postJournalPage = do | ||||
|     setMessage $ string $ printf "Added transaction:\n%s" (show t') | ||||
|     redirect RedirectTemporary JournalPage | ||||
| 
 | ||||
| getEditPage :: Handler HledgerWebApp RepHtml | ||||
| getEditPage = do | ||||
|     -- app <- getYesod | ||||
|     params <- getParams | ||||
|     -- t <- liftIO $ getCurrentLocalTime | ||||
|     let head' x = if null x then "" else head x | ||||
|         a = head' $ params "a" | ||||
|         p = head' $ params "p" | ||||
|         -- opts = appOpts app ++ [Period p] | ||||
|         -- args = appArgs app ++ [a] | ||||
|         -- fspec = optsToFilterSpec opts args t | ||||
|     -- reload journal's text, without parsing, if changed | ||||
|     j <- liftIO $ fromJust `fmap` getValue "hledger" "journal" | ||||
|     changed <- liftIO $ journalFileIsNewer j | ||||
|     -- XXX readFile may throw an error | ||||
|     s <- liftIO $ if changed then readFile (filepath j) else return (jtext j) | ||||
|     -- render the page | ||||
|     msg <- getMessage | ||||
|     Just here <- getRoute | ||||
|     hamletToRepHtml $ template' here msg a p "hledger" s | ||||
| 
 | ||||
| template' here msg a p title content = [$hamlet| | ||||
| !!! | ||||
| %html | ||||
|  %head | ||||
|   %title $string.title$ | ||||
|   %meta!http-equiv=Content-Type!content=$string.metacontent$ | ||||
|   %link!rel=stylesheet!type=text/css!href=@stylesheet@!media=all | ||||
|  %body | ||||
|   ^navbar'^ | ||||
|   #messages $m$ | ||||
|   ^editform'^ | ||||
| |] | ||||
|  where m = fromMaybe (string "") msg | ||||
|        navbar' = navbar here a p | ||||
|        stylesheet = StyleCss | ||||
|        metacontent = "text/html; charset=utf-8" | ||||
|        editform' = editform content | ||||
| 
 | ||||
| editform :: String -> Hamlet HledgerWebAppRoutes | ||||
| editform t = [$hamlet| | ||||
|  %form!method=POST | ||||
|   %table.form#editform!cellpadding=0!cellspacing=0!!border=0 | ||||
|    %tr.formheading | ||||
|     %td!colspan=2 | ||||
|      %span!style=float:right; ^formhelp^ | ||||
|      %span#formheading Edit journal: | ||||
|    %tr | ||||
|     %td!colspan=2 | ||||
|      %textarea!name=text!rows=50!cols=80 | ||||
|       $string.t$ | ||||
|    %tr#addbuttonrow | ||||
|     %td | ||||
|      %a!href=@JournalPage@ cancel | ||||
|     %td!align=right | ||||
|      %input!type=submit!value=$string.submitlabel$ | ||||
|    %tr.helprow | ||||
|     %td | ||||
|     %td!align=right | ||||
|      #help $string.edithelp$ | ||||
| |] | ||||
|  where | ||||
|   submitlabel = "save journal" | ||||
|   formhelp = helplink "file-format" "file format help" | ||||
|   edithelp = "Are you sure ? All previous data will be replaced" | ||||
| 
 | ||||
| postEditPage :: Handler HledgerWebApp RepPlain | ||||
| postEditPage = do | ||||
|   -- get form input values, or basic validation errors. E means an Either value. | ||||
|   textE  <- runFormPost $ catchFormError $ required $ input "text" | ||||
|   -- display errors or add transaction | ||||
|   case textE of | ||||
|    Left errs -> do | ||||
|     -- save current form values in session | ||||
|     setMessage $ string $ intercalate "; " $ map (intercalate ", " . map (\(a,b) -> a++": "++b)) [errs] | ||||
|     redirect RedirectTemporary JournalPage | ||||
| 
 | ||||
|    Right t' -> do | ||||
|     j <- liftIO $ fromJust `fmap` getValue "hledger" "journal" | ||||
|     filechanged' <- liftIO $ journalFileIsNewer j | ||||
|     let f = filepath j | ||||
|         told = jtext j | ||||
|         tnew = filter (/= '\r') t' | ||||
|         changed = tnew /= told || filechanged' | ||||
| --    changed <- liftIO $ writeFileWithBackupIfChanged f t'' | ||||
|     liftIO $ writeFileWithBackup f tnew | ||||
|     setMessage $ string $ if changed then printf "Saved journal to %s\n" (show f) | ||||
|                                      else "No change" | ||||
|     redirect RedirectTemporary JournalPage | ||||
| 
 | ||||
|  | ||||
| @ -13,14 +13,18 @@ module Hledger.Cli.Utils | ||||
|      journalReload, | ||||
|      journalReloadIfChanged, | ||||
|      journalFileModificationTime, | ||||
|      openBrowserOn | ||||
|      openBrowserOn, | ||||
|      writeFileWithBackup, | ||||
|      writeFileWithBackupIfChanged, | ||||
|     ) | ||||
| where | ||||
| import Hledger.Data | ||||
| import Hledger.Read | ||||
| import Hledger.Cli.Options (Opt(..),journalFilePathFromOpts) -- ,optsToFilterSpec) | ||||
| import System.Directory (doesFileExist, getModificationTime) | ||||
| import Safe (readMay) | ||||
| import System.Directory (doesFileExist, getModificationTime, getDirectoryContents, copyFile) | ||||
| import System.Exit | ||||
| import System.FilePath ((</>), splitFileName, takeDirectory) | ||||
| import System.Info (os) | ||||
| import System.Process (readProcessWithExitCode) | ||||
| import System.Time (ClockTime, getClockTime, diffClockTimes, TimeDiff(TimeDiff)) | ||||
| @ -100,3 +104,33 @@ openBrowserOn u = trybrowsers browsers u | ||||
|     -- ::ShellExecute(NULL, "open", "www.somepage.com", NULL, NULL, SW_SHOWNORMAL); | ||||
|     -- ::ShellExecute(NULL, "open", "firefox.exe", "www.somepage.com" NULL, SW_SHOWNORMAL); | ||||
| 
 | ||||
| -- | Back up this file with a (incrementing) numbered suffix then | ||||
| -- overwrite it with this new text, or give an error, but only if the text | ||||
| -- is different from the current file contents, and return a flag | ||||
| -- indicating whether we did anything. | ||||
| writeFileWithBackupIfChanged :: FilePath -> String -> IO Bool | ||||
| writeFileWithBackupIfChanged f t = do | ||||
|   s <- readFile f | ||||
|   if t == s then return False | ||||
|             else backUpFile f >> writeFile f t >> return True | ||||
| 
 | ||||
| -- | Back up this file with a (incrementing) numbered suffix, then | ||||
| -- overwrite it with this new text, or give an error. | ||||
| writeFileWithBackup :: FilePath -> String -> IO () | ||||
| writeFileWithBackup f t = backUpFile f >> writeFile f t | ||||
| 
 | ||||
| -- | Back up this file with a (incrementing) numbered suffix, or give an error. | ||||
| backUpFile :: FilePath -> IO () | ||||
| backUpFile fp = do | ||||
|   fs <- getDirectoryContents $ takeDirectory fp | ||||
|   let (d,f) = splitFileName fp | ||||
|       versions = catMaybes $ map (f `backupNumber`) fs | ||||
|       next = maximum (0:versions) + 1 | ||||
|       f' = printf "%s.%d" f next | ||||
|   copyFile fp (d </> f') | ||||
| 
 | ||||
| -- | Does the second file represent a backup of the first, and if so which version is it ? | ||||
| backupNumber :: FilePath -> FilePath -> Maybe Int | ||||
| backupNumber f g = case matchRegexPR ("^" ++ f ++ "\\.([0-9]+)$") g of | ||||
|                         Just (_, ((_,suffix):_)) -> readMay suffix | ||||
|                         _ -> Nothing | ||||
|  | ||||
| @ -9,11 +9,13 @@ body { font-family: "helvetica","arial", "sans serif"; margin:0; } | ||||
| #resetlink { font-size:small; } | ||||
| .toprightlink { font-size:small; margin-left:1em; float:right; } | ||||
| #messages { color:red; background-color:#ffeeee; margin:0.5em;} | ||||
| #addform { margin:1em; font-size:small; } | ||||
| .form { margin:1em; font-size:small; } | ||||
| #addform { background-color:#eeeeee; border:2px solid #dddddd; cell-padding:0; cell-spacing:0; } | ||||
| #addform { float:right; } | ||||
| #addform #descriptionrow { } | ||||
| #addform #postingrow { } | ||||
| #addform #addbuttonrow { text-align:right; } | ||||
| #editform textarea { background-color:#eeeeee; } | ||||
| #content { margin:1em; } | ||||
| .formheading td { padding-bottom:8px; } | ||||
| #formheading { font-size:medium; font-weight:bold; } | ||||
|  | ||||
| @ -28,7 +28,7 @@ import System.Directory (getHomeDirectory) | ||||
| import System.Environment (getEnv) | ||||
| import System.FilePath ((</>)) | ||||
| import System.Exit | ||||
| import System.IO (stderr) | ||||
| import System.IO (IOMode(..), withFile, hGetContents, stderr) | ||||
| #if __GLASGOW_HASKELL__ <= 610 | ||||
| import Prelude hiding (readFile, putStr, putStrLn, print, getContents) | ||||
| import System.IO.UTF8 | ||||
| @ -82,10 +82,10 @@ journalFromPathAndString format fp s = do | ||||
|                 fmt fs = intercalate ", " (init fs) ++ " or " ++ last fs ++ " " | ||||
| 
 | ||||
| -- | Read a journal from this file, using the specified data format or | ||||
| -- trying all known formats, or give an error. | ||||
| -- trying all known formats, or give an error (and ensure the file is closed). | ||||
| readJournalFile :: Maybe String -> FilePath -> IO Journal | ||||
| readJournalFile format "-" = getContents >>= journalFromPathAndString format "(stdin)" | ||||
| readJournalFile format f   = readFile f  >>= journalFromPathAndString format f | ||||
| readJournalFile format f   = withFile f ReadMode $ \h -> hGetContents h >>= journalFromPathAndString format f | ||||
| 
 | ||||
| -- | Read a Journal from this string, using the specified data format or | ||||
| -- trying all known formats, or give an error. | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user