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
|
/ IndexPage GET
|
||||||
/style.css StyleCss GET
|
/style.css StyleCss GET
|
||||||
/journal JournalPage GET POST
|
/journal JournalPage GET POST
|
||||||
|
/edit EditPage GET POST
|
||||||
/register RegisterPage GET
|
/register RegisterPage GET
|
||||||
/balance BalancePage GET
|
/balance BalancePage GET
|
||||||
|]
|
|]
|
||||||
@ -164,12 +165,14 @@ navbar here a p = [$hamlet|
|
|||||||
navlinks :: HledgerWebAppRoutes -> String -> String -> Hamlet HledgerWebAppRoutes
|
navlinks :: HledgerWebAppRoutes -> String -> String -> Hamlet HledgerWebAppRoutes
|
||||||
navlinks here a p = [$hamlet|
|
navlinks here a p = [$hamlet|
|
||||||
#navlinks
|
#navlinks
|
||||||
^journallink^ | $
|
^journallink^ $
|
||||||
^registerlink^ | $
|
(^editlink^) $
|
||||||
^balancelink^
|
| ^registerlink^ $
|
||||||
|
| ^balancelink^ $
|
||||||
|]
|
|]
|
||||||
where
|
where
|
||||||
journallink = navlink here "journal" JournalPage
|
journallink = navlink here "journal" JournalPage
|
||||||
|
editlink = navlink here "edit" EditPage
|
||||||
registerlink = navlink here "register" RegisterPage
|
registerlink = navlink here "register" RegisterPage
|
||||||
balancelink = navlink here "balance" BalancePage
|
balancelink = navlink here "balance" BalancePage
|
||||||
navlink here s dest = [$hamlet|%a.$style$!href=@?u@ $string.s$|]
|
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 HledgerWebAppRoutes
|
||||||
addform = [$hamlet|
|
addform = [$hamlet|
|
||||||
%form!method=POST
|
%form!method=POST
|
||||||
%table#addform!cellpadding=0!cellspacing=0!!border=0
|
%table.form#addform!cellpadding=0!cellspacing=0!!border=0
|
||||||
%tr.formheading
|
%tr.formheading
|
||||||
%td!colspan=4
|
%td!colspan=4
|
||||||
%span!style=float:right; ^formhelp^
|
|
||||||
%span#formheading Add a transaction:
|
%span#formheading Add a transaction:
|
||||||
%tr
|
%tr
|
||||||
%td!colspan=4
|
%td!colspan=4
|
||||||
@ -332,3 +334,93 @@ postJournalPage = do
|
|||||||
setMessage $ string $ printf "Added transaction:\n%s" (show t')
|
setMessage $ string $ printf "Added transaction:\n%s" (show t')
|
||||||
redirect RedirectTemporary JournalPage
|
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,
|
journalReload,
|
||||||
journalReloadIfChanged,
|
journalReloadIfChanged,
|
||||||
journalFileModificationTime,
|
journalFileModificationTime,
|
||||||
openBrowserOn
|
openBrowserOn,
|
||||||
|
writeFileWithBackup,
|
||||||
|
writeFileWithBackupIfChanged,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
import Hledger.Data
|
import Hledger.Data
|
||||||
import Hledger.Read
|
import Hledger.Read
|
||||||
import Hledger.Cli.Options (Opt(..),journalFilePathFromOpts) -- ,optsToFilterSpec)
|
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.Exit
|
||||||
|
import System.FilePath ((</>), splitFileName, takeDirectory)
|
||||||
import System.Info (os)
|
import System.Info (os)
|
||||||
import System.Process (readProcessWithExitCode)
|
import System.Process (readProcessWithExitCode)
|
||||||
import System.Time (ClockTime, getClockTime, diffClockTimes, TimeDiff(TimeDiff))
|
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", "www.somepage.com", NULL, NULL, SW_SHOWNORMAL);
|
||||||
-- ::ShellExecute(NULL, "open", "firefox.exe", "www.somepage.com" 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; }
|
#resetlink { font-size:small; }
|
||||||
.toprightlink { font-size:small; margin-left:1em; float:right; }
|
.toprightlink { font-size:small; margin-left:1em; float:right; }
|
||||||
#messages { color:red; background-color:#ffeeee; margin:0.5em;}
|
#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 { background-color:#eeeeee; border:2px solid #dddddd; cell-padding:0; cell-spacing:0; }
|
||||||
|
#addform { float:right; }
|
||||||
#addform #descriptionrow { }
|
#addform #descriptionrow { }
|
||||||
#addform #postingrow { }
|
#addform #postingrow { }
|
||||||
#addform #addbuttonrow { text-align:right; }
|
#addform #addbuttonrow { text-align:right; }
|
||||||
|
#editform textarea { background-color:#eeeeee; }
|
||||||
#content { margin:1em; }
|
#content { margin:1em; }
|
||||||
.formheading td { padding-bottom:8px; }
|
.formheading td { padding-bottom:8px; }
|
||||||
#formheading { font-size:medium; font-weight:bold; }
|
#formheading { font-size:medium; font-weight:bold; }
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import System.Directory (getHomeDirectory)
|
|||||||
import System.Environment (getEnv)
|
import System.Environment (getEnv)
|
||||||
import System.FilePath ((</>))
|
import System.FilePath ((</>))
|
||||||
import System.Exit
|
import System.Exit
|
||||||
import System.IO (stderr)
|
import System.IO (IOMode(..), withFile, hGetContents, stderr)
|
||||||
#if __GLASGOW_HASKELL__ <= 610
|
#if __GLASGOW_HASKELL__ <= 610
|
||||||
import Prelude hiding (readFile, putStr, putStrLn, print, getContents)
|
import Prelude hiding (readFile, putStr, putStrLn, print, getContents)
|
||||||
import System.IO.UTF8
|
import System.IO.UTF8
|
||||||
@ -82,10 +82,10 @@ journalFromPathAndString format fp s = do
|
|||||||
fmt fs = intercalate ", " (init fs) ++ " or " ++ last fs ++ " "
|
fmt fs = intercalate ", " (init fs) ++ " or " ++ last fs ++ " "
|
||||||
|
|
||||||
-- | Read a journal from this file, using the specified data format or
|
-- | 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 :: Maybe String -> FilePath -> IO Journal
|
||||||
readJournalFile format "-" = getContents >>= journalFromPathAndString format "(stdin)"
|
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
|
-- | Read a Journal from this string, using the specified data format or
|
||||||
-- trying all known formats, or give an error.
|
-- trying all known formats, or give an error.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user