Villefort 0.1.0.7 → 0.1.0.8
raw patch · 12 files changed
+254/−189 lines, 12 filesdep +randomPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: random
API changes (from Hackage documentation)
- Database: conv :: [[SqlValue]] -> [[String]]
- Lib: someFunc :: IO ()
- Stats: Query :: String -> Query
- Stats: data Query
- Stats: getAvg :: IO [[String]]
- Stats: getSum :: IO [[String]]
- Stats: hea :: String
- Stats: makeGithub :: String -> IO String
- Stats: makeRow :: [[Char]] -> [Char]
- Stats: makeTable :: [[[Char]]] -> [Char]
- Todo: Row :: Int -> String -> String -> String -> Int -> Row
- Todo: [description] :: Row -> String
- Todo: [due] :: Row -> String
- Todo: [rid] :: Row -> Int
- Todo: [time] :: Row -> Int
- Todo: [title] :: Row -> String
- Todo: addTime :: Int -> Int -> IO ()
- Todo: convTitle :: [Char] -> [Char]
- Todo: data Row
- Todo: daysTilDue :: [Char] -> IO Integer
- Todo: daysToColor' :: (Num a, Ord a) => a -> [Char]
- Todo: delTask :: Int -> IO ()
- Todo: fromZonedTimeToDay :: [Char] -> Day
- Todo: getModal :: IO [[Char]]
- Todo: merge :: [a] -> [a] -> [a]
- Todo: toRow :: [String] -> Row
+ Database: convRow :: [[SqlValue]] -> [[String]]
+ Todo: conv' :: SqlValue -> String
+ Todo: convRow' :: [[SqlValue]] -> [[String]]
+ Todo: makeQuery' :: String -> IO [[String]]
- Database: getSubjects :: IO [Subject]
+ Database: getSubjects :: IO [String]
- Database: makeQuery :: Query -> IO [[String]]
+ Database: makeQuery :: String -> IO [[String]]
Files
- README.md +5/−4
- Villefort.cabal +4/−3
- app/Main.hs +14/−9
- data/todo.db binary
- src/Database.hs +14/−6
- src/Lib.hs +0/−6
- src/Stats.hs +48/−7
- src/Todo.hs +39/−13
- templates/add.html +1/−30
- templates/header +125/−0
- templates/index.html +0/−109
- templates/modal.ts +4/−2
README.md view
@@ -1,10 +1,11 @@ # Villefort Villefort is a time managment system written in Haskell. -## What's new in version Villefort-0.1.0.7 -- Sqlite database now saves events using localtime. -- Days until due is now calculated using localtime. -- Tracks when you update working time on todo instead of just when you finish your task. +## What's new in version Villefort-0.1.0.8 +- Pretty colors on the stats page. +- Fixed time tracking bug. +- Better UX on the home page. + # Home screen  # Add new todos
Villefort.cabal view
@@ -1,5 +1,5 @@ name: Villefort-version: 0.1.0.7+version: 0.1.0.8 synopsis: Villefort is a task manager and time tracker written in haskell. description: Villefort is a browser based time tracker. homepage: https://github.com/Chrisr850/Villefort#readme@@ -15,13 +15,13 @@ data-files: data/todo.db , templates/*.html , templates/*.ts+ , templates/header , js.js library hs-source-dirs: src other-modules: Paths_Villefort- exposed-modules: Lib- , Database+ exposed-modules: Database , Todo , Stats build-depends: base >= 4.9 && < 5@@ -31,6 +31,7 @@ , time >= 1.6.0 && < 1.7 , FindBin >= 0.0.5 && < 0.1 , mtl >= 2.2.1 && < 2.3+ , random default-language: Haskell2010
app/Main.hs view
@@ -6,9 +6,9 @@ import Control.Monad.IO.Class import Database.HDBC.Sqlite3 import Database.HDBC-import Data.List.Split+import Data.List.Split import Data.Text.Lazy.Encoding (decodeUtf8)-import Data.Text.Lazy hiding (splitOn,map,concat)+import Data.Text.Lazy hiding (splitOn,map,concat,head) import Data.Time import Database import Todo@@ -40,12 +40,14 @@ makeNewPage = do- path <- getDataFileName "templates/add.html"- rawhtml <- liftIO $ readFile path- let split = splitOn "?" rawhtml+ headerPath <- getDataFileName "templates/header"+ header <- liftIO $ readFile headerPath+ addPath <- getDataFileName "templates/add.html"+ add <- liftIO $ readFile addPath+ let split = splitOn "?" add subjects <- getSubjects- let radiobuttons = map makeRadio subjects- return ((split !! 0) ++ (concat radiobuttons) ++ (split !! 1))+ let radiobuttons = map makeRadio subjects+ return (header ++ (split !! 0) ++ (concat radiobuttons) ++ (split !! 1)) -- | Main function of loop main :: IO ()@@ -86,8 +88,11 @@ let date = convDate $ getIndex parse 3 let title = rep $ getIndex parse 1 let subject = rep $ getIndex parse 2- stmt <- liftIO $ prepare conn "insert into todo (Description,Title,Entered,Due,State,time,Subject) Values (?,?,current_date,?,1,0,?)"- do liftIO $ execute stmt [toSql title,toSql summary,toSql date,toSql subject]+ f <-liftIO $ makeQuery "select id from todo order by id desc"+ let rawid = head $ f+ let id = (read (rawid !! 0) :: Integer) +1+ stmt <- liftIO $ prepare conn "insert into todo (id,Description,Title,Entered,Due,State,time,Subject) Values (?,?,?,current_date,?,1,0,?)"+ do liftIO $ execute stmt [toSql id,toSql title,toSql summary,toSql date,toSql subject] do liftIO $ commit conn do liftIO $ disconnect conn redirect "/"
data/todo.db view
binary file changed (12288 → 16384 bytes)
src/Database.hs view
@@ -14,7 +14,7 @@ type Subject = String -getSubjects :: IO [Subject]+--getSubjects :: IO (Maybe [Subject]) getSubjects = (\x-> (!! 0) <$> x) <$> makeQuery "select Subject from todo where state = 0 group by Subject" --path =fmap (\x -> (x !! 0) ++ "Villefort.app/Contents/Resources/") $ (Data.List.Split.splitOn "Villefort.app") <$> getProgPath@@ -25,15 +25,23 @@ path' = getDataDir getDb :: IO Connection -getDb = (++ "data/todo.db") <$> path >>= \path -> putStrLn path >> connectSqlite3 path+getDb = (++ "/data/todo.db") <$> path' >>= \path -> putStrLn path >> connectSqlite3 path -conv :: [[SqlValue]] -> [[String]]-conv dat = Prelude.map (\x -> Prelude.map (\y -> fromSql y :: String) x) dat+--convRow :: [[SqlValue]] -> [[String]]+convRow dat = Prelude.map (\x -> Prelude.map (\y -> fromSql y :: String ) x) dat -makeQuery :: Query -> IO [[String]]++--makeQuery :: Query -> IO [[String]]+makeQuery query = do+ conn <- getDb+ taskRaw <- quickQuery' conn query []+ disconnect conn+ return (convRow taskRaw)+ {- makeQuery query = getDb >>= \conn -> quickQuery' conn query [] >>=- \taskRaw -> disconnect conn >> return ( conv taskRaw)+ \taskRaw -> disconnect conn >> return ( convRow taskRaw) +-} execQuery :: Query -> [Int] -> IO ()
− src/Lib.hs
@@ -1,6 +0,0 @@-module Lib- ( someFunc- ) where--someFunc :: IO ()-someFunc = putStrLn "someFunc"
src/Stats.hs view
@@ -1,28 +1,69 @@-module Stats where+module Stats (genStats+ )where import Database import Control.Monad.IO.Class+import Paths_Villefort+import System.Random data Query = Query String +-- | Query to get average of subjects+--getAvg :: IO (Maybe [String]) getAvg = makeQuery "select avg(time), Subject from todo group by Subject order by avg(time) desc" +-- | Query to get sum of subjects+--getSum :: IO (Maybe [String]) getSum = makeQuery "select sum(time), Subject from todo group by Subject order by sum(time) desc" +-- | Helper function to generate row of table makeRow x = "<tr> <th>" ++ (x !! 0) ++ "</th> <th> " ++ (x!! 1) ++ "</th> </tr>" +-- | Generate Table makeTable stats = "<table style='width:100%'>" ++ (mconcat (map makeRow stats)) ++ "</table>" +-- | Generate stats -genStats = getSubjects >>= mapM makeGithub >>= \gits -> getAvg >>=- \avg -> getSum >>= \sum -> return (hea ++ (makeTable avg) ++ "</br> <h1> Sum </h1>" ++ (makeTable sum) ++ "</html>" ++ (mconcat gits)) -hea :: String-hea = "<!DOCTYPE html> <html> <script src='/js-chart-widgets.min.js'></script><h1> avg </h1>"+genStats = getSubjects >>= \x -> mapM makeGithub x >>= \gits -> getAvg >>=+ \avg -> getSum >>= \sum -> getDataFileName "templates/header" >>= \x -> liftIO (readFile x) >>= \head -> return (head ++ table ++ (makeTable avg) ++ "</br> <h1> Sum </h1>" ++ (makeTable sum) ++ "</html>" ++ (mconcat gits))+-- | Library to include for github table+table :: String+table = " <script src='/js-chart-widgets.min.js'></script><h1> avg </h1>" -makeGithub :: String -> IO String+-- | creates the github like graph from database+++--makeGithub :: Maybe String -> IO String makeGithub subject = do z <- makeQuery ("select Due from todo where subject = '" ++ subject ++ "' and state = 0")+ color <- getColor let head = "<h2 id='"++ subject ++ "'> Calendar "++ subject ++ " </h2> <script> new Calendar({ append_to: '" ++ subject ++ "',num_weeks: 51,day_size: 11, data: [" let q = Prelude.map (\x -> "['" ++ (x !! 0) ++"',500],") z- let bot = " ], color: '#005500' }); </script>"+ let bot = " ], color: " ++ color ++ " }); </script>" return (head ++ (Prelude.concat q )++ bot)++colors = ["'#F44336'"+ ,"'#E91E63'"+ ,"'#9C27B0'"+ ,"'#673AB7'"+ ,"'#3F51B5'"+ ,"'#2196F3'"+ ,"'#03A9F4'"+ ,"'#00BCD4'"+ ,"'#009688'"+ ,"'#4CAF50'"+ ,"'#8BC34A'"+ ,"'#CDDC39'"+ ,"'#FFEB3B'"+ ,"'#FFC107'"+ ,"'#FF9800'"+ ,"'#FF5722'"+ ,"'#795548'"+ ,"'#9E9E9E'"+ ,"'#607D8B'"+ ]++getColor = do+ number <- randomRIO (0, (length $ colors)-1) :: IO Int+ return (colors !! number)+
src/Todo.hs view
@@ -1,4 +1,12 @@-module Todo where+module Todo (getTodos+ ,updateTask+ ,deleteTodo+ ,qetTasks'+ ,makeQuery'+ ,conv'+ ,convRow'+ ,genModal'+ ) where import Database import Data.List.Split@@ -6,6 +14,8 @@ import Data.Time import Data.List.Split as S import Paths_Villefort+import Database.HDBC.Sqlite3 +import Database.HDBC data Row = Row { rid :: Int,@@ -15,22 +25,36 @@ time :: Int } deriving (Show) -toRow :: [String] -> Row-toRow x = Row (read (x !! 0) :: Int) (x !! 1) (x !! 2) (x !! 3) (read( x !! 4) :: Int)+--toRow :: Maybe [String] -> Maybe Row+toRow x = Row (read (x !! 0) :: Int) (x !! 1) (x !! 2) (x !! 3) (read( x !! 4) :: Int) -qetTasks' = makeQuery " select id, Title, Description, Due, sum(time) from todo where state = 1 group by Title, Description order by Due " >>=- \x -> return (map toRow x)+qetTasks' = do+ -- I would like to take this moment to personally apoligize to the sql gods for creating this monstrosity.+ x <- makeQuery' "select id, Title, Description, Due, sum(time) from todo where state=1 group by id "+ return (map toRow x) +convRow' dat = Prelude.map (\x -> Prelude.map (\y -> conv' y ) x) dat +--conv :: SqlValue -> String+conv' x = case fromSql x of+ Just x -> fromSql x :: String+ Nothing -> "1" +--makeQuery :: Query -> IO [[String]]+makeQuery' query = do+ conn <- getDb+ taskRaw <- quickQuery' conn query []+ disconnect conn+ return (convRow' taskRaw) + merge [] ys = ys merge (x:xs) ys = x:merge ys xs -genModal' row = do+genModal' row = if rid row == 1 then return (" ") else do let f = due row modal <- getModal days <- daysTilDue f@@ -40,11 +64,11 @@ show $ rid row, title row, description row,- "/delete", show $ time row,+ "/delete", show $ rid row ]- + return $ mconcat $ merge modal da @@ -90,16 +114,18 @@ fromZonedTimeToDay x = fromGregorian (read (nums !! 0) :: Integer) (read (nums !! 1) :: Int) (read (nums !! 2) :: Int) where nums = S.splitOn "-" $ take 10 x -+-- | Returns html from todos getTodos = do tasks <- qetTasks'+ do liftIO $ putStrLn $ show tasks path <- path - modals <- sequence $ Prelude.map genModal' tasks- path <- getDataFileName "templates/index.html"- header <- readFile path+ modals <- sequence $ genModal' <$> tasks+ headerPath <- getDataFileName "templates/header"+ header <- liftIO $ readFile headerPath let body = Prelude.concat modals return (header ++ body) +-- | Delete a done task from database sets state = 0 but it's record is still maintained in the database for the stats page. deleteTodo raw = do let da = Data.List.Split.splitOn "&" (show raw) let rawid = Data.List.Split.splitOn "=" $ (Prelude.init (da !! 1))@@ -118,6 +144,6 @@ delTask id = execQuery "update todo set state = 0 where id = ?" [id] updateTask :: Int -> Int -> IO ()-updateTask id time = execQuery "insert into todo (Description,Title,Entered,Due,state,time,Subject) select Description,Title,Entered,Due,0,time+?,Subject from todo where id = ?" [ time, id]+updateTask id time = execQuery "insert into todo (id,Description,Title,Entered,Due,state,time,Subject) select id,Description,Title,Entered,Due,1,?,Subject from todo where id = ? limit 1" [ time, id]
templates/add.html view
@@ -1,40 +1,11 @@-<!doctype html>-<title>Villefort</title>- <meta charset="utf-8">- <meta name="viewport" content="width=device-width, initial-scale=1">-<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='static1.css') }}">-<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> + <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>- <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script src="https://code.jquery.com/jquery-1.10.2.min.js" integrity="sha256-C6CB9UYIS9UJeqinPHWTHVqh/E1uhG5Twh+Y5qFQmYg=" crossorigin="anonymous"></script>- <style>- .page {- padding-left:20%;- }- </style> -<div class="page">- <h1>Villefort</h1>- - - <div align="left">- <div class="btn-group">- <div class="dropdown pull-left ">- <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Todo - <span id="i1" class="caret"></span></button>- <ul class="dropdown-menu">- <li><a href="/">Home</a></li>- <li><a href="/new">New</a></li>- <li><a href="/stat"> Log </a> </li>- </ul>- </div>- </div>- </div> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script> <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css">
+ templates/header view
@@ -0,0 +1,125 @@+<!doctype html>+<title>Villefort</title>+ <meta charset="utf-8">+ + <meta name="viewport" content="width=device-width, initial-scale=1">+<link rel="shortcut icon" href="/static/favicon.ico"> ++ <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>+ <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>+ <style>+ + + /*+ .btn-due7{+ background: #750787;+ color: #ffffff;+ }+ .btn-due6{+ background: #750787;+ color: #ffffff;+ }+ .btn-due5{+background: #008026;+ color: #ffffff;+ }+ .btn-due4{+background: #004DFF;+ color: #ffffff;+ }+ .btn-due3{+background: #008026;+ color: #ffffff;+ }+ .btn-due2{+background: #FFED00;+ color: #ffffff;+ }+ .btn-due1{+background: #FF8C00;+ color: #ffffff;+ }+ .btn-due0{+ background: #E40303;+ color: #ffffff;+ }+ */+ .btn-due11{ background: #3F51B5; color: #ffffff;}.btn-due10{ background: #E8EAF6; color: #ffffff;}.btn-due9{ background: #C5CAE9; color: #ffffff;}.btn-due8{ background: #9FA8DA; color: #ffffff;}.btn-due7{ background: #7986CB; color: #ffffff;}.btn-due6{ background: #5C6BC0; color: #ffffff;}.btn-due5{ background: #3F51B5; color: #ffffff;}.btn-due4{ background: #3949AB; color: #ffffff;}.btn-due3{ background: #303F9F; color: #ffffff;}.btn-due2{ background: #283593; color: #ffffff;}.btn-due1{ background: #1A237E; color: #ffffff;}+ /*+ .btn-due7{+ background: #311B92;+ color: #ffffff;+ }+ .btn-due6{+ background: #750787;+ color: #ffffff;+ }+ .btn-due5{+background: #008026;+ color: #ffffff;+ }+ .btn-due4{+background: #004DFF;+ color: #ffffff;+ }+ .btn-due3{ +background: #008026;+ color: #ffffff;+ }+ .btn-due2{+background: #FFED00;+ color: #ffffff;+ }+ .btn-due1{+background: #FF8C00;+ color: #ffffff;+ }+ .btn-due0{+ background: #311B92;+ color: #ffffff;+ }+ */+ </style>++<div class="page">+ <h1>Villefort</h1>+ + + <div class ="todo">+ <div align="left">+ <div class="btn-group">+ <div class="dropdown pull-left ">+ <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Todo + <span id="i1" class="caret"></span></button>+ <ul class="dropdown-menu">+ <li><a href="/">Home</a></li>+ <li><a href="/new">New</a></li>+ <li><a href="/stat"> Log </a> </li>+ </ul>+ </div>+ + </div>+ </div>++ </br>+ + </div> +<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">+ <script+ src="https://code.jquery.com/jquery-1.10.2.min.js"+ integrity="sha256-C6CB9UYIS9UJeqinPHWTHVqh/E1uhG5Twh+Y5qFQmYg="+ crossorigin="anonymous"></script>+ <style>+ .page {+ padding-left:20%;+ }+ </style> +<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>+<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>+ <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css">+ <script>+ $(document).ready(function() {+ $("#due").datepicker();+ });+ </script>+
templates/index.html view
@@ -1,109 +0,0 @@-<!doctype html>-<title>Villefort</title>- <meta charset="utf-8">- - <meta name="viewport" content="width=device-width, initial-scale=1">-<link rel="shortcut icon" href="/static/favicon.ico"> -<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>- <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>- <style>- .todo {- padding-left:20%;- }- h1 {- padding-left:20%;- }- /*- .btn-due7{- background: #750787;- color: #ffffff;- }- .btn-due6{- background: #750787;- color: #ffffff;- }- .btn-due5{-background: #008026;- color: #ffffff;- }- .btn-due4{-background: #004DFF;- color: #ffffff;- }- .btn-due3{-background: #008026;- color: #ffffff;- }- .btn-due2{-background: #FFED00;- color: #ffffff;- }- .btn-due1{-background: #FF8C00;- color: #ffffff;- }- .btn-due0{- background: #E40303;- color: #ffffff;- }- */- .btn-due11{ background: #3F51B5; color: #ffffff;}.btn-due10{ background: #E8EAF6; color: #ffffff;}.btn-due9{ background: #C5CAE9; color: #ffffff;}.btn-due8{ background: #9FA8DA; color: #ffffff;}.btn-due7{ background: #7986CB; color: #ffffff;}.btn-due6{ background: #5C6BC0; color: #ffffff;}.btn-due5{ background: #3F51B5; color: #ffffff;}.btn-due4{ background: #3949AB; color: #ffffff;}.btn-due3{ background: #303F9F; color: #ffffff;}.btn-due2{ background: #283593; color: #ffffff;}.btn-due1{ background: #1A237E; color: #ffffff;}- /*- .btn-due7{- background: #311B92;- color: #ffffff;- }- .btn-due6{- background: #750787;- color: #ffffff;- }- .btn-due5{-background: #008026;- color: #ffffff;- }- .btn-due4{-background: #004DFF;- color: #ffffff;- }- .btn-due3{ -background: #008026;- color: #ffffff;- }- .btn-due2{-background: #FFED00;- color: #ffffff;- }- .btn-due1{-background: #FF8C00;- color: #ffffff;- }- .btn-due0{- background: #311B92;- color: #ffffff;- }- */- </style>--<div class="page">- <h1>Villefort</h1>- - - <div class ="todo">- <div align="left">- <div class="btn-group">- <div class="dropdown pull-left ">- <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Todo - <span id="i1" class="caret"></span></button>- <ul class="dropdown-menu">- <li><a href="/">Home</a></li>- <li><a href="/new">New</a></li>- <li><a href="/stat"> Log </a> </li>- </ul>- </div>- - </div>- </div>-- </br>-
templates/modal.ts view
@@ -13,14 +13,16 @@ </div> <div class="modal-body"> <p> } </p>+ + <p> Total minutes spent working on Project } </p> </div> <div class="modal-footer"> <form class="pull-left" action="}" method="post" class="add-entry"> <dl> <dd><button type="submit" class="btn btn-success pull-left" >Finished</button>- Time <input type="text" name="time" value="}"> <br>+ Time <input type="text" name="time" value="0"> <input type="hidden" class="pull-left" name="id" value="}">- <button type="submit" formaction="/update">update</button>+ <button type="submit" class="btn btn-warning pull-right" style="margin-left: 20px;" formaction="/update">update</button> </dl> </form>