By | Chris Done |
At | 2011-06-08 |
Title | Updated paste form, page caching. |
Description |
Edit file amelie.cabal 33188 → 33188
11 11 Build-type: Simple
12 12 Cabal-version: >=1.2
13 13 14 14 Executable amelie
15 15 Main-is: Main.hs
- 16 Ghc-options: -threaded -O0
+ 16 Ghc-options: -threaded -O2
17 17 Hs-source-dirs: src
18 18 Build-depends: base >= 4 && < 5
19 19 ,snap-server >= 0.4 && < 0.5
20 20 ,snap-core >= 0.4 && < 0.5
21 21 ,text >= 0.11 && < 0.12
… … … … Edit file src/Main.hs 33188 → 33188
3 3 -- | Main entry point.
4 4 5 5 module Main (main) where
6 6 7 7 import Amelie.Controller
+ 8 import Amelie.Controller.Cache (newCache)
8 9 import Amelie.Controller.Home as Home
+ 10 import Amelie.Controller.New as New
9 11 import Amelie.Controller.Paste as Paste
10 12 import Amelie.Controller.Script as Script
11 13 import Amelie.Controller.Style as Style
12 14 import Amelie.Model.Config (auth)
+ 15 import Amelie.Types
+ 16 import Amelie.Types.Cache
13 17 14 18 import Snap.Http.Server
15 19 import Snap.Types
16 20 import Snap.Util.FileServe
17 21 … … … … 19 23 20 24 -- | Main entry point.
21 25 main :: IO ()
22 26 main = do
23 27 p <- newPool auth
+ 28 cache <- newCache
24 29 setUnicodeLocale "en_US"
- 25 httpServe config (serve p)
+ 30 httpServe config (serve p cache)
26 31 where config = addListen (ListenHttp "0.0.0.0" 10000) defaultConfig
27 32 28 33 -- | Serve the controllers.
- 29 serve :: Pool -> Snap ()
- 30 serve p = route routes where
+ 34 serve :: Pool -> Cache -> Snap ()
+ 35 serve p cache = route routes where
31 36 routes = [("/css/amelie.css", run Style.handle)
32 37 ,("/js/amelie.js", run Script.handle)
33 38 ,("/css/",serveDirectory "wwwroot/css")
34 39 ,("/js/",serveDirectory "wwwroot/js")
35 40 ,("",run Home.handle)
36 41 ,("/:id",run Paste.handle)
+ 42 ,("/new",run New.handle)
37 43 ]
- 38 run = runHandler p
+ 44 run = runHandler p cache
… … … … Edit file src/Text/Formlet.hs 33188 → 33188
1 1 {-# OPTIONS -Wall #-}
2 2 {-# LANGUAGE OverloadedStrings, RecordWildCards, FlexibleInstances #-}
+ 3 {-# LANGUAGE ViewPatterns #-}
3 4 {-# OPTIONS -fno-warn-name-shadowing -fno-warn-orphans #-}
4 5 5 6 -- | Mini formlets library.
6 7 7 8 module Text.Formlet
8 9 (Formlet(..)
9 10 ,formlet
10 11 ,req
11 12 ,opt
12 13 ,wrap
+ 14 ,integer
13 15 ,textInput
14 16 ,dropInput
15 17 ,areaInput
- 16 ,submitInput) where
+ 18 ,submitInput
+ 19 ,parse) where
17 20 18 21 import Control.Applicative
19 22 import Control.Monad.Error
20 23 import Control.Monad.Reader
21 24 import Control.Monad.Trans.Error (ErrorList(..))
… … … … 25 28 import Data.Monoid.Operator
26 29 import Data.Text (Text)
27 30 import qualified Data.Text as T
28 31 import Data.Text.Encoding
29 32 import Prelude hiding ((++))
+ 33 import Safe (readMay)
30 34 import Snap.Types
31 35 import Text.Blaze.Html5 as H
32 36 import qualified Text.Blaze.Html5.Attributes as A
33 37 34 38 -- | A simple formlet data type, fails on first error.
… … … … 100 104 case formletValue inputs of
101 105 Right v | T.null v -> Right Nothing
102 106 meh -> Just <$> meh
103 107 }
104 108 + 109 + 110 + 111 -- | Parse a form value.
+ 112 parse :: (a -> Either Text b) -> Formlet a -> Formlet b
+ 113 parse parser formlet@Formlet{..} =
+ 114 formlet { formletValue = \inputs ->
+ 115 case formletValue inputs of
+ 116 Left e -> Left e
+ 117 Right x -> case parser x of
+ 118 Right y -> Right y
+ 119 Left e -> Left [e ++ maybe "" (": "++) formletName]
+ 120 }
+ 121 + 122 -- | Integer parser.
+ 123 integer :: Text -> Either Text Integer
+ 124 integer (readMay . T.unpack -> Just v) = Right v
+ 125 integer _ = Left "expected integer"
+ 126 105 127 -- | Wrap/transform formlet's HTML.
106 128 wrap :: (Html -> Html) -> Formlet Text -> Formlet Text
107 129 wrap f formlet@Formlet{..} = formlet { formletHtml = f . formletHtml }
108 130 109 131 -- | Make a text input formlet with a label.
… … … … 123 145 p $ H.label $ do
124 146 H.span $ toHtml $ caption ++ ": "
125 147 textarea ! A.name (toValue name) $ toHtml $ fromMaybe "" value
126 148 127 149 -- | Make a drop down input with a label.
- 128 dropInput :: Text -> Text -> Formlet Text
- 129 dropInput name caption =
+ 150 dropInput :: [(Text,Text)] -> Text -> Text -> Formlet Text
+ 151 dropInput values name caption =
130 152 formlet name $ \value -> do
131 153 p $ H.label $ do
132 154 H.span $ toHtml $ caption ++ ": "
- 133 input ! A.name (toValue name)
- 134 ! A.value (toValue $ fromMaybe "" value)
+ 155 select ! A.name (toValue name) $
+ 156 forM_ values $ \(key,title) -> do
+ 157 let selected | Just key == value = (! A.selected "selected")
+ 158 | otherwise = id
+ 159 selected $ option ! A.value (toValue key) $ toHtml title
135 160 136 161 -- | Make a submit (captioned) button.
137 162 submitInput :: Text -> Text -> Html
138 163 submitInput name caption = p $ do
139 164 p $ H.input ! A.type_ "submit"
… … … … Edit file src/Amelie/Controller.hs 33188 → 33188
7 7 ,output
8 8 ,outputText)
9 9 where
10 10 11 11 import Amelie.Types
+ 12 import Amelie.Types.Cache
12 13 13 14 import Control.Monad.Reader (runReaderT)
14 15 import Data.Text.Lazy (Text,toStrict)
15 16 import Database.PostgreSQL.Simple (Pool,withPoolConnection)
16 17 import Snap.Types (Snap,writeText)
17 18 import Text.Blaze (Html)
18 19 import Text.Blaze.Renderer.Text (renderHtml)
19 20 20 21 -- | Run a controller handler.
- 21 runHandler :: Pool -> Controller () -> Snap ()
- 22 runHandler pool ctrl = do
+ 22 runHandler :: Pool -> Cache -> Controller () -> Snap ()
+ 23 runHandler pool cache ctrl = do
23 24 withPoolConnection pool $ \conn -> do
- 24 let state = ControllerState conn
+ 25 let state = ControllerState conn cache
25 26 runReaderT (runController ctrl) state
26 27 27 28 -- | Strictly renders HTML to Text before outputting it via Snap.
28 29 -- This ensures that any lazy exceptions are caught by the Snap
29 30 -- handler.
… … … … Edit file src/Amelie/Types.hs 33188 → 33188
3 3 -- | All types.
4 4 5 5 module Amelie.Types
6 6 (module Amelie.Types.MVC
7 7 ,module Amelie.Types.Paste
+ 8 ,module Amelie.Types.Channel
+ 9 ,module Amelie.Types.Language
8 10 ,module Amelie.Types.Page
9 11 ,module Amelie.Types.Newtypes)
10 12 where
11 13 12 14 import Amelie.Types.MVC
13 15 import Amelie.Types.Paste
+ 16 import Amelie.Types.Channel
+ 17 import Amelie.Types.Language
14 18 import Amelie.Types.Page
15 19 import Amelie.Types.Newtypes
… … … … Edit file src/Amelie/View/Home.hs 33188 → 33188
9 9 where
10 10 11 11 import Amelie.Types
12 12 import Amelie.View.Html
13 13 import Amelie.View.Layout
- 14 import Amelie.View.Paste (pasteLink)
+ 14 import Amelie.View.Paste (pasteLink)
15 15 + 16 import Control.Arrow ((&&&))
16 17 import Data.Maybe (fromMaybe)
17 18 import Data.Time.Show (showDateTime)
18 19 import Prelude hiding ((++))
19 20 import Text.Blaze.Html5 as H hiding (map)
20 21 import qualified Text.Blaze.Html5.Attributes as A
21 22 22 23 -- | Render the home page.
- 23 page :: [Paste] -> Html -> Html
- 24 page ps form =
+ 24 page :: [Channel] -> [Language] -> [Paste] -> Html -> Html
+ 25 page chans langs ps form =
25 26 layoutPage $ Page {
- 26 pageTitle = "λ Knights!"
- 27 , pageBody = content ps form
+ 27 pageTitle = "Recent pastes"
+ 28 , pageBody = content chans langs ps form
28 29 , pageName = "home"
29 30 }
30 31 31 32 -- | Render the home page body.
- 32 content :: [Paste] -> Html -> Html
- 33 content ps form = do
+ 33 content :: [Channel] -> [Language] -> [Paste] -> Html -> Html
+ 34 content chans langs ps form = do
34 35 createNew form
- 35 latest ps
+ 36 latest chans langs ps
36 37 37 38 -- | Create a new paste section.
38 39 createNew :: Html -> Html
39 40 createNew = lightSection "Create new paste"
40 41 41 42 -- | View the latest pastes.
- 42 latest :: [Paste] -> Html
- 43 latest ps = do
+ 43 latest :: [Channel] -> [Language] -> [Paste] -> Html
+ 44 latest channels languages ps = do
44 45 darkSection "Latest pastes" $
45 46 table ! A.width "100%" $ do
46 47 tr $ mapM_ (th . toHtml) $ words "Title Author When Language Channel"
47 48 pastes ps
48 49 49 50 where pastes = mapM_ $ \paste@Paste{..} -> tr $ do
50 51 td $ pasteLink paste pasteTitle
51 52 td $ toHtml pasteAuthor
52 53 td $ toHtml $ showDateTime $ pasteDate
- 53 td $ toHtml $ fromMaybe "-" pasteLanguage
- 54 td $ toHtml $ fromMaybe "-" pasteChannel
+ 54 td $ toHtml $ fromMaybe "-" (pasteLanguage >>= (`lookup` langs))
+ 55 td $ toHtml $ fromMaybe "-" (pasteChannel >>= (`lookup` chans))
+ 56 chans = map (channelId &&& channelName) channels
+ 57 langs = map (languageId &&& languageTitle) languages
… … … … Edit file src/Amelie/View/Layout.hs 33188 → 33188
23 23 html $ do
24 24 meta ! A.httpEquiv "Content-Type" ! A.content "text/html; charset=UTF-8"
25 25 link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/css/amelie.css"
26 26 js "jquery.js"
27 27 js "amelie.js"
- 28 title $ toHtml $ pageTitle
+ 28 title $ toHtml $ pageTitle ++ " :: hpaste — Haskell Pastebin"
29 29 body ! A.id (toValue pageName) $
30 30 wrap $ do
31 31 logo
32 32 pageBody
33 33 … … … … Edit file src/Amelie/View/Paste.hs 33188 → 33188
15 15 import Amelie.View.Html
16 16 import Amelie.View.Layout
17 17 18 18 import Control.Applicative ((<$>),(<*>),pure)
19 19 import Control.Monad (when)
- 20 import Data.ByteString.UTF8
+ 20 import Data.ByteString.UTF8 (toString)
21 21 import qualified Data.Map as M
22 22 import Data.Monoid.Operator ((++))
+ 23 import Data.String (fromString)
23 24 import Data.Text (Text)
24 25 import Data.Text.Encoding (encodeUtf8)
25 26 import Data.Text.Lazy (fromStrict)
26 27 import Data.Time.Show (showDateTime)
+ 28 import Data.Traversable
27 29 import Prelude hiding ((++))
28 30 import Safe (readMay)
- 29 import Snap.Types (Params)
30 31 import Text.Blaze.Html5 as H hiding (map)
+ 32 import qualified Text.Blaze.Html5.Attributes as A
31 33 import Text.Blaze.Html5.Extra
32 34 import Text.Formlet
33 35 import Text.Highlighter.Formatters.Html (format)
34 36 import Text.Highlighter.Lexer (runLexer)
35 37 import Text.Highlighter.Lexers.Haskell (lexer)
36 38 37 39 -- | A formlet for paste submission / editing.
- 38 pasteFormlet :: Params -> Bool -> [Text] -> (Formlet PasteSubmit,Html)
- 39 pasteFormlet params submitted errors =
- 40 let form = postForm $ do
- 41 when submitted $
- 42 when (not (null errors)) $
+ 40 pasteFormlet :: PasteFormlet -> (Formlet PasteSubmit,Html)
+ 41 pasteFormlet PasteFormlet{..} =
+ 42 let form = postForm ! A.action "/new" $ do
+ 43 when pfSubmitted $
+ 44 when (not (null pfErrors)) $
43 45 H.div ! aClass "errors" $
- 44 mapM_ (p . toHtml) errors
- 45 formletHtml formlet params
+ 46 mapM_ (p . toHtml) pfErrors
+ 47 formletHtml formlet pfParams
46 48 submitInput "submit" "Submit"
47 49 in (formlet,form)
48 50 49 51 where formlet =
- 50 PasteSubmit <$> pure pasteId
- 51 <*> req (textInput "title" "Title")
- 52 <*> req (textInput "author" "Author")
- 53 <*> pure Nothing -- opt (dropInput "language" "Language")
- 54 <*> pure Nothing -- opt (dropInput "channel" "Channel")
- 55 <*> req (areaInput "paste" "Paste")
- 56 pasteId = M.lookup "paste_id" params >>=
+ 52 PasteSubmit
+ 53 <$> pure pasteId
+ 54 <*> req (textInput "title" "Title")
+ 55 <*> req (textInput "author" "Author")
+ 56 <*> parse (traverse (fmap LanguageId . integer))
+ 57 (opt (dropInput languages "language" "Language"))
+ 58 <*> parse (traverse (fmap ChannelId . integer))
+ 59 (opt (dropInput channels "channel" "Channel"))
+ 60 <*> req (areaInput "paste" "Paste")
+ 61 + 62 pasteId = M.lookup "paste_id" pfParams >>=
57 63 readMay . concat . map toString >>=
58 64 return . (fromIntegral :: Integer -> PasteId)
+ 65 channels = map (\Channel{..} ->
+ 66 (fromString $ show channelId,channelName))
+ 67 pfChannels
+ 68 languages = map (\Language{..} ->
+ 69 (fromString $ show languageId,languageTitle))
+ 70 pfLanguages
59 71 60 72 -- | Render the page page.
61 73 page :: Paste -> Html
62 74 page p@Paste{..} =
63 75 layoutPage $ Page {
… … … … 78 90 darkSection (fromStrict pasteTitle) $ do
79 91 ul ! aClass "paste-specs" $ do
80 92 detail "Paste" $ pasteLink paste $ "#" ++ show pasteId
81 93 detail "Author" $ pasteAuthor
82 94 detail "Channel" $ maybe "-" show pasteChannel
+ 95 detail "Language" $ maybe "-" show pasteLanguage
83 96 detail "Created" $ showDateTime pasteDate
84 97 detail "Raw" $ pasteRawLink paste $ ("View raw link" :: Text)
85 98 clear
86 99 87 100 where detail title content = do
… … … … 91 104 pasteContent Paste{..} =
92 105 lightNoTitleSection $ do
93 106 case runLexer lexer (encodeUtf8 (pastePaste ++ "\n")) of
94 107 Right tokens -> format True tokens
95 108 _ -> pre $ toHtml pastePaste
- 96 97 109 98 110 -- | The href link to a paste.
99 111 pasteLink :: ToHtml html => Paste -> html -> Html
100 112 pasteLink Paste{..} inner = href ("/" ++ show pasteId) inner
101 113 … … … … Edit file src/Amelie/View/Style.hs 33188 → 33188
168 168 -- | The line number part.
169 169 lineNumbers :: CSS (Either Property Rule)
170 170 lineNumbers = do
171 171 subRule ".linenodiv" $ do
172 172 margin "0 1em 0 0"
+ 173 textAlign "right"
173 174 174 175 subRule "a" $ do
175 176 textDecoration "none"
176 177 color "#555"
177 178 178 179 -- | Home page styles.
179 180 home :: CSS Rule
180 181 home = do
- 181 rule "#home" $ do
- 182 subRule ".amelie-wrap" $ do
- 183 width "50em"
+ 182 rule "#home" wrap
+ 183 rule "#new" wrap
+ 184 + 185 where wrap = subRule ".amelie-wrap" $ do
+ 186 width "50em"
… … … … Edit file src/Amelie/Types/MVC.hs 33188 → 33188
9 9 ,Model(..)
10 10 ,ControllerState(..)
11 11 ,ModelState(..))
12 12 where
13 13 + 14 import Amelie.Types.Cache
+ 15 14 16 import Control.Applicative (Applicative,Alternative)
15 17 import Control.Monad (MonadPlus)
16 18 import Control.Monad.Catch (MonadCatchIO)
17 19 import Control.Monad.Reader (ReaderT,MonadReader)
18 20 import Control.Monad.Trans (MonadIO)
19 21 import Database.PostgreSQL.Simple (Connection)
20 22 import Snap.Types (Snap,MonadSnap)
21 23 22 24 -- | The state accessible to the controller (DB/session stuff).
23 25 data ControllerState = ControllerState {
- 24 controllerStateConn :: Connection
+ 26 controllerStateConn :: Connection
+ 27 , controllerStateCache :: Cache
25 28 }
26 29 27 30 -- | The controller monad.
28 31 newtype Controller a = Controller {
29 32 runController :: ReaderT ControllerState Snap a
… … … … Edit file src/Amelie/Types/Newtypes.hs 33188 → 33188
2 2 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
3 3 4 4 -- | Newtypes; foreign keys and such.
5 5 6 6 module Amelie.Types.Newtypes
- 7 (PasteId(..))
+ 7 (PasteId(..)
+ 8 ,ChannelId(..)
+ 9 ,LanguageId(..))
8 10 where
9 11 10 12 import Database.PostgreSQL.Simple.Result (Result)
11 13 import Database.PostgreSQL.Simple.Param (Param)
12 14 13 15 newtype PasteId = PasteId Integer
14 16 deriving (Integral,Real,Num,Ord,Eq,Enum,Result,Param)
15 17 16 18 instance Show PasteId where show (PasteId pid) = show pid
+ 19 + 20 newtype ChannelId = ChannelId Integer
+ 21 deriving (Integral,Real,Num,Ord,Eq,Enum,Result,Param)
+ 22 + 23 instance Show ChannelId where show (ChannelId pid) = show pid
+ 24 + 25 newtype LanguageId = LanguageId Integer
+ 26 deriving (Integral,Real,Num,Ord,Eq,Enum,Result,Param)
+ 27 + 28 instance Show LanguageId where show (LanguageId pid) = show pid
… … … … Edit file src/Amelie/Types/Paste.hs 33188 → 33188
5 5 6 6 -- | The paste type.
7 7 8 8 module Amelie.Types.Paste
9 9 (Paste(..)
- 10 ,PasteSubmit(..))
+ 10 ,PasteSubmit(..)
+ 11 ,PasteFormlet(..))
11 12 where
12 13 13 14 import Amelie.Types.Newtypes
+ 15 import Amelie.Types.Language
+ 16 import Amelie.Types.Channel
14 17 15 18 import Data.Text (Text,pack)
16 19 import Data.Time (UTCTime,zonedTimeToUTC)
17 20 import Database.PostgreSQL.Simple.QueryResults (QueryResults(..))
+ 21 import Snap.Types (Params)
18 22 import Text.Blaze (ToHtml(..),toHtml)
19 23 20 24 -- | A paste.
21 25 data Paste = Paste {
22 26 pasteId :: PasteId
23 27 ,pasteTitle :: Text
24 28 ,pasteDate :: UTCTime
25 29 ,pasteAuthor :: Text
- 26 ,pasteLanguage :: Maybe Text
- 27 ,pasteChannel :: Maybe Text
+ 30 ,pasteLanguage :: Maybe LanguageId
+ 31 ,pasteChannel :: Maybe ChannelId
28 32 ,pastePaste :: Text
29 33 ,pasteViews :: Integer
30 34 ,pasteParent :: Maybe PasteId
31 35 } deriving Show
32 36 33 37 -- | A paste submission or edit.
34 38 data PasteSubmit = PasteSubmit {
35 39 pasteSubmitId :: Maybe PasteId
36 40 ,pasteSubmitTitle :: Text
37 41 ,pasteSubmitAuthor :: Text
- 38 ,pasteSubmitLanguage :: Maybe Text
- 39 ,pasteSubmitChannel :: Maybe Text
+ 42 ,pasteSubmitLanguage :: Maybe LanguageId
+ 43 ,pasteSubmitChannel :: Maybe ChannelId
40 44 ,pasteSubmitPaste :: Text
41 45 } deriving Show
42 46 43 47 instance ToHtml Paste where
44 48 toHtml paste@Paste{..} = toHtml $ pack $ show paste
… … … … 55 59 , pasteViews = views
56 60 , pasteParent = parent
57 61 }
58 62 where (pid,title,content,author,date,views,language,channel,parent) =
59 63 convertResults field values
+ 64 + 65 data PasteFormlet = PasteFormlet {
+ 66 pfSubmitted :: Bool
+ 67 , pfErrors :: [Text]
+ 68 , pfParams :: Params
+ 69 , pfLanguages :: [Language]
+ 70 , pfChannels :: [Channel]
+ 71 }
… … … … Remove file src/Amelie/Model/Home.hs 33188
- 1 {-# OPTIONS -Wall #-} - 2 {-# LANGUAGE OverloadedStrings #-} - 3 - 4 -- | Home page model. - 5 - 6 module Amelie.Model.Home - 7 (getPastes) - 8 where - 9 - 10 import Amelie.Model - 11 import Amelie.Model.Paste - 12 - 13 getPastes :: Model [Paste] - 14 getPastes = getLatestPastes Edit file src/Amelie/Model/Paste.hs 33188 → 33188
42 42 43 43 -- | Create a new paste.
44 44 createPaste :: PasteSubmit -> Model (Maybe PasteId)
45 45 createPaste PasteSubmit{..} =
46 46 single ["INSERT INTO paste"
- 47 ,"(title,author,content)"
+ 47 ,"(title,author,content,channel,language)"
48 48 ,"VALUES"
- 49 ,"(?,?,?)"
+ 49 ,"(?,?,?,?,?)"
50 50 ,"returning id"]
- 51 (pasteSubmitTitle,pasteSubmitAuthor,pasteSubmitPaste)
+ 51 (pasteSubmitTitle,pasteSubmitAuthor,pasteSubmitPaste
+ 52 ,pasteSubmitChannel,pasteSubmitLanguage)
52 53 53 54 -- | Update an existing paste.
54 55 updatePaste :: PasteId -> PasteSubmit -> Model ()
55 56 updatePaste pid PasteSubmit{..} = do
56 57 _ <- exec (["UPDATE paste"
57 58 ,"SET"]
58 59 ++
- 59 map set (words "title author content")
+ 60 map set (words "title author content language channel")
60 61 ++
61 62 ["WHERE id = ?"])
62 63 (pasteSubmitTitle
63 64 ,pasteSubmitAuthor
64 65 ,pasteSubmitPaste
+ 66 ,pasteSubmitLanguage
+ 67 ,pasteSubmitChannel
65 68 ,pid)
66 69 return ()
67 70 68 71 where set key = unwords [key,"=","?"]
… … … … Edit file src/Amelie/Controller/Home.hs 33188 → 33188
5 5 6 6 module Amelie.Controller.Home
7 7 (handle)
8 8 where
9 9 - 10 import Amelie.Controller (output)
+ 10 import Amelie.Controller (outputText)
+ 11 import Amelie.Controller.Cache (cache)
11 12 import Amelie.Controller.Paste (pasteForm)
12 13 import Amelie.Model
- 13 import Amelie.Model.Home (getPastes)
+ 14 import Amelie.Model.Channel (getChannels)
+ 15 import Amelie.Model.Language (getLanguages)
+ 16 import Amelie.Model.Paste (getLatestPastes)
+ 17 import Amelie.Types.Cache as Key
14 18 import Amelie.View.Home (page)
15 19 16 20 handle :: Controller ()
17 21 handle = do
- 18 pastes <- model $ getPastes
- 19 form <- pasteForm
- 20 output $ page pastes form
+ 22 html <- cache Key.Home $ do
+ 23 pastes <- model $ getLatestPastes
+ 24 chans <- model $ getChannels
+ 25 langs <- model $ getLanguages
+ 26 form <- pasteForm chans langs
+ 27 return $ Just $ page chans langs pastes form
+ 28 maybe (return ()) outputText html
… … … … Edit file src/Amelie/Controller/Paste.hs 33188 → 33188
9 9 ,pasteForm)
10 10 where
11 11 12 12 import Amelie.Types
13 13 + 14 import Amelie.Controller
+ 15 import Amelie.Controller.Cache (cache,resetCache)
14 16 import Amelie.Model
- 15 import Amelie.Model.Paste (createOrEdit,getPasteById)
- 16 import Amelie.View.Paste (pasteFormlet,page)
- 17 import Amelie.Controller
+ 17 import Amelie.Model.Paste (createOrEdit,getPasteById)
+ 18 import Amelie.Types.Cache as Key
+ 19 import Amelie.View.Paste (pasteFormlet,page)
18 20 19 21 import Control.Applicative
- 20 import Data.ByteString.UTF8 (toString)
+ 22 import Data.ByteString.UTF8 (toString)
21 23 import Data.Maybe
- 22 import Data.Monoid.Operator ((++))
- 23 import Data.String (fromString)
- 24 import Prelude hiding ((++))
+ 24 import Data.Monoid.Operator ((++))
+ 25 import Data.String (fromString)
+ 26 import Prelude hiding ((++))
25 27 import Safe
26 28 import Snap.Types
- 27 import Text.Blaze.Html5 as H hiding (output)
+ 29 import Text.Blaze.Html5 as H hiding (output)
28 30 import Text.Formlet
29 31 30 32 -- | Handle the paste page.
31 33 handle :: Controller ()
32 34 handle = do
33 35 pid <- (>>= readMay) . fmap (toString) <$> getParam "id"
34 36 case pid of
35 37 Nothing -> goHome
36 38 Just (pid :: Integer) -> do
- 37 paste <- model $ getPasteById (fromIntegral pid)
- 38 maybe goHome (output . page) paste
+ 39 html <- cache (Key.Paste pid) $ do
+ 40 paste <- model $ getPasteById (fromIntegral pid)
+ 41 return $ page <$> paste
+ 42 case html of
+ 43 Just html -> outputText html
+ 44 Nothing -> goHome
39 45 40 46 where goHome = redirect "/"
41 47 42 48 -- | Control paste editing / submission.
- 43 pasteForm :: Controller Html
- 44 pasteForm = do
+ 49 pasteForm :: [Channel] -> [Language] -> Controller Html
+ 50 pasteForm channels languages = do
45 51 params <- getParams
46 52 submitted <- isJust <$> getParam "submit"
- 47 let (getValue,_) = pasteFormlet params submitted []
+ 53 let formlet = PasteFormlet {
+ 54 pfSubmitted = submitted
+ 55 , pfErrors = []
+ 56 , pfParams = params
+ 57 , pfChannels = channels
+ 58 , pfLanguages = languages
+ 59 }
+ 60 (getValue,_) = pasteFormlet formlet
48 61 value = formletValue getValue params
49 62 errors = either id (const []) value
- 50 (_,html) = pasteFormlet params submitted errors
+ 63 (_,html) = pasteFormlet formlet { pfErrors = errors }
51 64 val = either (const Nothing) Just $ value
52 65 case val of
53 66 Nothing -> return ()
54 67 Just paste -> do
+ 68 resetCache Key.Home
55 69 pid <- model $ createOrEdit paste
56 70 maybe (return ()) redirectToPaste pid
57 71 return html
58 72 59 73 -- | Redirect to the paste's page.
… … … … Add file src/Amelie/View/New.hs 33188
+ 1 {-# OPTIONS -Wall -fno-warn-name-shadowing #-} + 2 {-# LANGUAGE OverloadedStrings #-} + 3 {-# LANGUAGE RecordWildCards #-} + 4 + 5 -- | Create new paste view. + 6 + 7 module Amelie.View.New + 8 (page) + 9 where + 10 + 11 import Amelie.Types + 12 import Amelie.View.Html + 13 import Amelie.View.Layout + 14 + 15 import Prelude hiding ((++)) + 16 import Text.Blaze.Html5 as H hiding (map) + 17 + 18 -- | Render the create new paste page. + 19 page :: Html -> Html + 20 page form = + 21 layoutPage $ Page { + 22 pageTitle = "Create new paste" + 23 , pageBody = lightSection "Create new paste" form + 24 , pageName = "new" + 25 } Add file src/Amelie/Types/Cache.hs 33188
+ 1 {-# OPTIONS -Wall #-} + 2 + 3 -- | HTML caching types. + 4 + 5 module Amelie.Types.Cache + 6 (Key(..) + 7 ,Cache(..)) + 8 where + 9 + 10 import Control.Concurrent.MVar (MVar) + 11 import Data.Map (Map) + 12 import Data.Text.Lazy (Text) + 13 + 14 data Key = + 15 Home + 16 | Paste Integer + 17 deriving (Eq,Ord) + 18 + 19 data Cache = + 20 Cache { + 21 cacheMap :: MVar (Map Key Text) + 22 } Add file src/Amelie/Types/Channel.hs 33188
+ 1 {-# OPTIONS -Wall #-} + 2 + 3 -- | The channel type. + 4 + 5 module Amelie.Types.Channel + 6 (Channel(..)) + 7 where + 8 + 9 import Amelie.Types.Newtypes + 10 + 11 import Data.Text (Text) + 12 import Database.PostgreSQL.Simple.QueryResults (QueryResults(..)) + 13 + 14 data Channel = Channel { + 15 channelId :: ChannelId + 16 ,channelName :: Text + 17 } deriving Show + 18 + 19 instance QueryResults Channel where + 20 convertResults field values = Channel { + 21 channelId = cid + 22 , channelName = name + 23 } + 24 where (cid,name) = convertResults field values Add file src/Amelie/Types/Language.hs 33188
+ 1 {-# OPTIONS -Wall #-} + 2 + 3 -- | The language type. + 4 + 5 module Amelie.Types.Language + 6 (Language(..)) + 7 where + 8 + 9 import Amelie.Types.Newtypes + 10 + 11 import Data.Text (Text) + 12 import Database.PostgreSQL.Simple.QueryResults (QueryResults(..)) + 13 + 14 data Language = Language { + 15 languageId :: LanguageId + 16 ,languageName :: Text + 17 ,languageTitle :: Text + 18 } deriving Show + 19 + 20 instance QueryResults Language where + 21 convertResults field values = Language { + 22 languageName = name + 23 , languageId = lid + 24 , languageTitle = title + 25 } + 26 where (lid,name,title) = convertResults field values Add file src/Amelie/Model/Channel.hs 33188
+ 1 {-# OPTIONS -Wall #-} + 2 {-# LANGUAGE OverloadedStrings #-} + 3 {-# LANGUAGE RecordWildCards #-} + 4 + 5 -- | Channel model. + 6 + 7 module Amelie.Model.Channel + 8 (getChannels) + 9 where + 10 + 11 import Amelie.Types + 12 import Amelie.Model + 13 + 14 -- | Get the channels. + 15 getChannels :: Model [Channel] + 16 getChannels = + 17 queryNoParams ["SELECT *" + 18 ,"FROM channel"] Add file src/Amelie/Model/Language.hs 33188
+ 1 {-# OPTIONS -Wall #-} + 2 {-# LANGUAGE OverloadedStrings #-} + 3 {-# LANGUAGE RecordWildCards #-} + 4 + 5 -- | Language model. + 6 + 7 module Amelie.Model.Language + 8 (getLanguages) + 9 where + 10 + 11 import Amelie.Types + 12 import Amelie.Model + 13 + 14 -- | Get the languages. + 15 getLanguages :: Model [Language] + 16 getLanguages = + 17 queryNoParams ["SELECT *" + 18 ,"FROM language"] Add file src/Amelie/Controller/Cache.hs 33188
+ 1 {-# OPTIONS -Wall -fno-warn-name-shadowing #-} + 2 + 3 -- | HTML caching. + 4 + 5 module Amelie.Controller.Cache + 6 (newCache + 7 ,cache + 8 ,resetCache) + 9 where + 10 + 11 import Amelie.Types + 12 import Amelie.Types.Cache + 13 + 14 import Control.Applicative ((<$>)) + 15 import Control.Concurrent + 16 import Control.Monad.IO (io) + 17 import Control.Monad.Reader (asks) + 18 import qualified Data.Map as M + 19 import Data.Text.Lazy (Text) + 20 import Text.Blaze.Html5 (Html) + 21 import Text.Blaze.Renderer.Text (renderHtml) + 22 + 23 -- | Create a new cache. + 24 newCache :: IO Cache + 25 newCache = do + 26 var <- newMVar M.empty + 27 return $ Cache var + 28 + 29 -- | Generate and save into the cache, or retrieve existing from the + 30 -- | cache. + 31 cache :: Key -> Controller (Maybe Html) -> Controller (Maybe Text) + 32 cache key generate = do + 33 Cache var <- asks controllerStateCache + 34 mapping <- io $ readMVar var + 35 case M.lookup key mapping of + 36 Just html -> return $ Just html + 37 Nothing -> do + 38 html <- fmap renderHtml <$> generate + 39 case html of + 40 Just html -> io $ modifyMVar_ var (return . M.insert key html) + 41 Nothing -> return () + 42 return $ html + 43 + 44 -- | Reset an item in the cache. + 45 resetCache :: Key -> Controller () + 46 resetCache key = do + 47 Cache var <- asks controllerStateCache + 48 io $ modifyMVar_ var (return . M.delete key) Add file src/Amelie/Controller/New.hs 33188
+ 1 {-# OPTIONS -Wall #-} + 2 {-# LANGUAGE OverloadedStrings #-} + 3 + 4 -- | Create new paste controller. + 5 + 6 module Amelie.Controller.New + 7 (handle) + 8 where + 9 + 10 import Amelie.Controller + 11 import Amelie.Controller.Paste (pasteForm) + 12 import Amelie.Model + 13 import Amelie.Model.Channel (getChannels) + 14 import Amelie.Model.Language (getLanguages) + 15 import Amelie.View.New (page) + 16 + 17 handle :: Controller () + 18 handle = do + 19 chans <- model $ getChannels + 20 langs <- model $ getLanguages + 21 form <- pasteForm chans langs + 22 output $ page form