diff options
| author | Gregor Kleen <gkleen@yggdrasil.li> | 2017-03-14 01:06:28 +0100 |
|---|---|---|
| committer | Gregor Kleen <gkleen@yggdrasil.li> | 2017-03-14 01:06:28 +0100 |
| commit | d84b462a711ce95593ff05a7581e722562c3835a (patch) | |
| tree | 41e5af455fea925b2680b29718b24ba2876e803a | |
| download | bar-d84b462a711ce95593ff05a7581e722562c3835a.tar bar-d84b462a711ce95593ff05a7581e722562c3835a.tar.gz bar-d84b462a711ce95593ff05a7581e722562c3835a.tar.bz2 bar-d84b462a711ce95593ff05a7581e722562c3835a.tar.xz bar-d84b462a711ce95593ff05a7581e722562c3835a.zip | |
Implement old bar.hs
41 files changed, 1608 insertions, 0 deletions
diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..a44395f --- /dev/null +++ b/.dir-locals.el | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | ((haskell-mode . ((haskell-indent-spaces . 4) | ||
| 2 | (haskell-process-use-ghci . t))) | ||
| 3 | (hamlet-mode . ((hamlet/basic-offset . 4) | ||
| 4 | (haskell-process-use-ghci . t)))) | ||
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5747830 --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | dist* | ||
| 2 | static/tmp/ | ||
| 3 | static/combined/ | ||
| 4 | config/client_session_key.aes | ||
| 5 | *.hi | ||
| 6 | *.o | ||
| 7 | *.sqlite3 | ||
| 8 | *.sqlite3-shm | ||
| 9 | *.sqlite3-wal | ||
| 10 | .hsenv* | ||
| 11 | cabal-dev/ | ||
| 12 | .stack-work/ | ||
| 13 | yesod-devel/ | ||
| 14 | .cabal-sandbox | ||
| 15 | cabal.sandbox.config | ||
| 16 | .DS_Store | ||
| 17 | *.swp | ||
| 18 | *.keter | ||
| 19 | **/result | ||
| 20 | **/client_session_key.aes | ||
| 21 | **/.gup | ||
| 22 | static/webshim/ | ||
| 23 | static/jquery.js | ||
diff --git a/Application.hs b/Application.hs new file mode 100644 index 0000000..048a316 --- /dev/null +++ b/Application.hs | |||
| @@ -0,0 +1,180 @@ | |||
| 1 | {-# OPTIONS_GHC -fno-warn-orphans #-} | ||
| 2 | module Application | ||
| 3 | ( getApplicationDev | ||
| 4 | , appMain | ||
| 5 | , develMain | ||
| 6 | , makeFoundation | ||
| 7 | , makeLogWare | ||
| 8 | -- * for DevelMain | ||
| 9 | , getApplicationRepl | ||
| 10 | , shutdownApp | ||
| 11 | -- * for GHCI | ||
| 12 | , handler | ||
| 13 | , db | ||
| 14 | ) where | ||
| 15 | |||
| 16 | import Control.Monad.Logger (liftLoc, runLoggingT) | ||
| 17 | import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr, | ||
| 18 | pgPoolSize, runSqlPool) | ||
| 19 | import Import | ||
| 20 | import Language.Haskell.TH.Syntax (qLocation) | ||
| 21 | import Network.Wai (Middleware) | ||
| 22 | import Network.Wai.Handler.Warp (Settings, defaultSettings, | ||
| 23 | defaultShouldDisplayException, | ||
| 24 | runSettings, setHost, | ||
| 25 | setOnException, setPort, getPort) | ||
| 26 | import Network.Wai.Middleware.RequestLogger (Destination (Logger), | ||
| 27 | IPAddrSource (..), | ||
| 28 | OutputFormat (..), destination, | ||
| 29 | mkRequestLogger, outputFormat) | ||
| 30 | import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet, | ||
| 31 | toLogStr) | ||
| 32 | |||
| 33 | -- Import all relevant handler modules here. | ||
| 34 | -- Don't forget to add new modules to your cabal file! | ||
| 35 | import Handler.InventoryListing | ||
| 36 | import Handler.UpdateItem | ||
| 37 | import Handler.OpenItem | ||
| 38 | import Handler.DeleteItem | ||
| 39 | import Handler.Item | ||
| 40 | |||
| 41 | -- This line actually creates our YesodDispatch instance. It is the second half | ||
| 42 | -- of the call to mkYesodData which occurs in Foundation.hs. Please see the | ||
| 43 | -- comments there for more details. | ||
| 44 | mkYesodDispatch "App" resourcesApp | ||
| 45 | |||
| 46 | -- | This function allocates resources (such as a database connection pool), | ||
| 47 | -- performs initialization and returns a foundation datatype value. This is also | ||
| 48 | -- the place to put your migrate statements to have automatic database | ||
| 49 | -- migrations handled by Yesod. | ||
| 50 | makeFoundation :: AppSettings -> IO App | ||
| 51 | makeFoundation appSettings = do | ||
| 52 | -- Some basic initializations: HTTP connection manager, logger, and static | ||
| 53 | -- subsite. | ||
| 54 | appHttpManager <- newManager | ||
| 55 | appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger | ||
| 56 | |||
| 57 | -- We need a log function to create a connection pool. We need a connection | ||
| 58 | -- pool to create our foundation. And we need our foundation to get a | ||
| 59 | -- logging function. To get out of this loop, we initially create a | ||
| 60 | -- temporary foundation without a real connection pool, get a log function | ||
| 61 | -- from there, and then create the real foundation. | ||
| 62 | let mkFoundation appConnPool = App { appStatic = eStatic, ..} | ||
| 63 | -- The App {..} syntax is an example of record wild cards. For more | ||
| 64 | -- information, see: | ||
| 65 | -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html | ||
| 66 | tempFoundation = mkFoundation $ error "connPool forced in tempFoundation" | ||
| 67 | logFunc = messageLoggerSource tempFoundation appLogger | ||
| 68 | |||
| 69 | -- Create the database connection pool | ||
| 70 | pool <- flip runLoggingT logFunc $ createPostgresqlPool | ||
| 71 | (pgConnStr $ appDatabaseConf appSettings) | ||
| 72 | (pgPoolSize $ appDatabaseConf appSettings) | ||
| 73 | |||
| 74 | -- Perform database migration using our application's logging settings. | ||
| 75 | runLoggingT (runSqlPool (runMigration migrateAll) pool) logFunc | ||
| 76 | |||
| 77 | -- Return the foundation | ||
| 78 | return $ mkFoundation pool | ||
| 79 | |||
| 80 | -- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and | ||
| 81 | -- applying some additional middlewares. | ||
| 82 | makeApplication :: App -> IO Application | ||
| 83 | makeApplication foundation = do | ||
| 84 | logWare <- makeLogWare foundation | ||
| 85 | -- Create the WAI application and apply middlewares | ||
| 86 | appPlain <- toWaiAppPlain foundation | ||
| 87 | return $ logWare $ defaultMiddlewaresNoLogging appPlain | ||
| 88 | |||
| 89 | makeLogWare :: App -> IO Middleware | ||
| 90 | makeLogWare foundation = | ||
| 91 | mkRequestLogger def | ||
| 92 | { outputFormat = | ||
| 93 | if appDetailedRequestLogging $ appSettings foundation | ||
| 94 | then Detailed True | ||
| 95 | else Apache | ||
| 96 | (if appIpFromHeader $ appSettings foundation | ||
| 97 | then FromFallback | ||
| 98 | else FromSocket) | ||
| 99 | , destination = Logger $ loggerSet $ appLogger foundation | ||
| 100 | } | ||
| 101 | |||
| 102 | |||
| 103 | -- | Warp settings for the given foundation value. | ||
| 104 | warpSettings :: App -> Settings | ||
| 105 | warpSettings foundation = | ||
| 106 | setPort (appPort $ appSettings foundation) | ||
| 107 | $ setHost (appHost $ appSettings foundation) | ||
| 108 | $ setOnException (\_req e -> | ||
| 109 | when (defaultShouldDisplayException e) $ messageLoggerSource | ||
| 110 | foundation | ||
| 111 | (appLogger foundation) | ||
| 112 | $(qLocation >>= liftLoc) | ||
| 113 | "yesod" | ||
| 114 | LevelError | ||
| 115 | (toLogStr $ "Exception from Warp: " ++ show e)) | ||
| 116 | defaultSettings | ||
| 117 | |||
| 118 | -- | For yesod devel, return the Warp settings and WAI Application. | ||
| 119 | getApplicationDev :: IO (Settings, Application) | ||
| 120 | getApplicationDev = do | ||
| 121 | settings <- getAppSettings | ||
| 122 | foundation <- makeFoundation settings | ||
| 123 | wsettings <- getDevSettings $ warpSettings foundation | ||
| 124 | app <- makeApplication foundation | ||
| 125 | return (wsettings, app) | ||
| 126 | |||
| 127 | getAppSettings :: IO AppSettings | ||
| 128 | getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv | ||
| 129 | |||
| 130 | -- | main function for use by yesod devel | ||
| 131 | develMain :: IO () | ||
| 132 | develMain = develMainHelper getApplicationDev | ||
| 133 | |||
| 134 | -- | The @main@ function for an executable running this site. | ||
| 135 | appMain :: IO () | ||
| 136 | appMain = do | ||
| 137 | -- Get the settings from all relevant sources | ||
| 138 | settings <- loadYamlSettingsArgs | ||
| 139 | -- fall back to compile-time values, set to [] to require values at runtime | ||
| 140 | [configSettingsYmlValue] | ||
| 141 | |||
| 142 | -- allow environment variables to override | ||
| 143 | useEnv | ||
| 144 | |||
| 145 | -- Generate the foundation from the settings | ||
| 146 | foundation <- makeFoundation settings | ||
| 147 | |||
| 148 | -- Generate a WAI Application from the foundation | ||
| 149 | app <- makeApplication foundation | ||
| 150 | |||
| 151 | -- Run the application with Warp | ||
| 152 | runSettings (warpSettings foundation) app | ||
| 153 | |||
| 154 | |||
| 155 | -------------------------------------------------------------- | ||
| 156 | -- Functions for DevelMain.hs (a way to run the app from GHCi) | ||
| 157 | -------------------------------------------------------------- | ||
| 158 | getApplicationRepl :: IO (Int, App, Application) | ||
| 159 | getApplicationRepl = do | ||
| 160 | settings <- getAppSettings | ||
| 161 | foundation <- makeFoundation settings | ||
| 162 | wsettings <- getDevSettings $ warpSettings foundation | ||
| 163 | app1 <- makeApplication foundation | ||
| 164 | return (getPort wsettings, foundation, app1) | ||
| 165 | |||
| 166 | shutdownApp :: App -> IO () | ||
| 167 | shutdownApp _ = return () | ||
| 168 | |||
| 169 | |||
| 170 | --------------------------------------------- | ||
| 171 | -- Functions for use in development with GHCi | ||
| 172 | --------------------------------------------- | ||
| 173 | |||
| 174 | -- | Run a handler | ||
| 175 | handler :: Handler a -> IO a | ||
| 176 | handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h | ||
| 177 | |||
| 178 | -- | Run DB queries | ||
| 179 | db :: ReaderT SqlBackend (HandlerT App IO) a -> IO a | ||
| 180 | db = handler . runDB | ||
diff --git a/Foundation.hs b/Foundation.hs new file mode 100644 index 0000000..85512a3 --- /dev/null +++ b/Foundation.hs | |||
| @@ -0,0 +1,141 @@ | |||
| 1 | module Foundation where | ||
| 2 | |||
| 3 | import Import.NoFoundation | ||
| 4 | import Database.Persist.Sql (ConnectionPool, runSqlPool) | ||
| 5 | import Text.Hamlet (hamletFile) | ||
| 6 | |||
| 7 | import Yesod.Core.Types (Logger) | ||
| 8 | import qualified Yesod.Core.Unsafe as Unsafe | ||
| 9 | |||
| 10 | -- | The foundation datatype for your application. This can be a good place to | ||
| 11 | -- keep settings and values requiring initialization before your application | ||
| 12 | -- starts running, such as database connections. Every handler will have | ||
| 13 | -- access to the data present here. | ||
| 14 | data App = App | ||
| 15 | { appSettings :: AppSettings | ||
| 16 | , appStatic :: EmbeddedStatic | ||
| 17 | , appConnPool :: ConnectionPool -- ^ Database connection pool. | ||
| 18 | , appHttpManager :: Manager | ||
| 19 | , appLogger :: Logger | ||
| 20 | } | ||
| 21 | |||
| 22 | data MenuItem = MenuItem | ||
| 23 | { menuItemLabel :: Text | ||
| 24 | , menuItemRoute :: Route App | ||
| 25 | } | ||
| 26 | |||
| 27 | -- This is where we define all of the routes in our application. For a full | ||
| 28 | -- explanation of the syntax, please see: | ||
| 29 | -- http://www.yesodweb.com/book/routing-and-handlers | ||
| 30 | -- | ||
| 31 | -- Note that this is really half the story; in Application.hs, mkYesodDispatch | ||
| 32 | -- generates the rest of the code. Please see the following documentation | ||
| 33 | -- for an explanation for this split: | ||
| 34 | -- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules | ||
| 35 | -- | ||
| 36 | -- This function also generates the following type synonyms: | ||
| 37 | -- type Handler = HandlerT App IO | ||
| 38 | -- type Widget = WidgetT App IO () | ||
| 39 | mkYesodData "App" $(parseRoutesFile "config/routes") | ||
| 40 | |||
| 41 | -- | A convenient synonym for creating forms. | ||
| 42 | type Form x = Html -> MForm (HandlerT App IO) (FormResult x, Widget) | ||
| 43 | |||
| 44 | -- Please see the documentation for the Yesod typeclass. There are a number | ||
| 45 | -- of settings which can be configured by overriding methods here. | ||
| 46 | instance Yesod App where | ||
| 47 | -- Controls the base of generated URLs. For more information on modifying, | ||
| 48 | -- see: https://github.com/yesodweb/yesod/wiki/Overriding-approot | ||
| 49 | approot = ApprootRequest $ \app req -> | ||
| 50 | case appRoot $ appSettings app of | ||
| 51 | Nothing -> getApprootText guessApproot app req | ||
| 52 | Just root -> root | ||
| 53 | |||
| 54 | -- Store session data on the client in encrypted cookies, | ||
| 55 | -- default session idle timeout is 120 minutes | ||
| 56 | makeSessionBackend _ = Just <$> defaultClientSessionBackend | ||
| 57 | 120 -- timeout in minutes | ||
| 58 | "client_session_key.aes" | ||
| 59 | |||
| 60 | -- Yesod Middleware allows you to run code before and after each handler function. | ||
| 61 | |||
| 62 | -- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks. | ||
| 63 | -- Some users may also want to add the defaultCsrfMiddleware, which: | ||
| 64 | -- a) Sets a cookie with a CSRF token in it. | ||
| 65 | -- b) Validates that incoming write requests include that token in either a header or POST parameter. | ||
| 66 | -- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware | ||
| 67 | -- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package. | ||
| 68 | yesodMiddleware = defaultYesodMiddleware | ||
| 69 | |||
| 70 | defaultLayout widget = do | ||
| 71 | msgs <- getMessages | ||
| 72 | |||
| 73 | mCurrentRoute <- getCurrentRoute | ||
| 74 | |||
| 75 | -- Define the menu items of the header. | ||
| 76 | let menuItems = | ||
| 77 | [ MenuItem "Inventory" InventoryListingR | ||
| 78 | ] | ||
| 79 | |||
| 80 | -- We break up the default layout into two components: | ||
| 81 | -- default-layout is the contents of the body tag, and | ||
| 82 | -- default-layout-wrapper is the entire page. Since the final | ||
| 83 | -- value passed to hamletToRepHtml cannot be a widget, this allows | ||
| 84 | -- you to use normal widget features in default-layout. | ||
| 85 | |||
| 86 | pc <- widgetToPageContent $ do | ||
| 87 | addScript $ StaticR jquery_js | ||
| 88 | addScript $ StaticR webshim_polyfiller_js | ||
| 89 | $(widgetFile "default-layout") | ||
| 90 | withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") | ||
| 91 | |||
| 92 | |||
| 93 | -- This function creates static content files in the static folder | ||
| 94 | -- and names them based on a hash of their content. This allows | ||
| 95 | -- expiration dates to be set far in the future without worry of | ||
| 96 | -- users receiving stale content. | ||
| 97 | addStaticContent = embedStaticContent appStatic StaticR Right | ||
| 98 | |||
| 99 | -- What messages should be logged. The following includes all messages when | ||
| 100 | -- in development, and warnings and errors in production. | ||
| 101 | shouldLog app _source level = | ||
| 102 | appShouldLogAll (appSettings app) | ||
| 103 | || level == LevelWarn | ||
| 104 | || level == LevelError | ||
| 105 | |||
| 106 | makeLogger = return . appLogger | ||
| 107 | |||
| 108 | -- Provide proper Bootstrap styling for default displays, like | ||
| 109 | -- error pages | ||
| 110 | defaultMessageWidget title body = $(widgetFile "default-message-widget") | ||
| 111 | |||
| 112 | -- How to run database actions. | ||
| 113 | instance YesodPersist App where | ||
| 114 | type YesodPersistBackend App = SqlBackend | ||
| 115 | runDB action = do | ||
| 116 | master <- getYesod | ||
| 117 | runSqlPool action $ appConnPool master | ||
| 118 | instance YesodPersistRunner App where | ||
| 119 | getDBRunner = defaultGetDBRunner appConnPool | ||
| 120 | |||
| 121 | -- This instance is required to use forms. You can modify renderMessage to | ||
| 122 | -- achieve customized and internationalized form validation messages. | ||
| 123 | instance RenderMessage App FormMessage where | ||
| 124 | renderMessage _ _ = defaultFormMessage | ||
| 125 | |||
| 126 | -- Useful when writing code that is re-usable outside of the Handler context. | ||
| 127 | -- An example is background jobs that send email. | ||
| 128 | -- This can also be useful for writing code that works across multiple Yesod applications. | ||
| 129 | instance HasHttpManager App where | ||
| 130 | getHttpManager = appHttpManager | ||
| 131 | |||
| 132 | unsafeHandler :: App -> Handler a -> IO a | ||
| 133 | unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger | ||
| 134 | |||
| 135 | -- Note: Some functionality previously present in the scaffolding has been | ||
| 136 | -- moved to documentation in the Wiki. Following are some hopefully helpful | ||
| 137 | -- links: | ||
| 138 | -- | ||
| 139 | -- https://github.com/yesodweb/yesod/wiki/Sending-email | ||
| 140 | -- https://github.com/yesodweb/yesod/wiki/Serve-static-files-from-a-separate-domain | ||
| 141 | -- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding | ||
diff --git a/Handler/Common.hs b/Handler/Common.hs new file mode 100644 index 0000000..38fb1ce --- /dev/null +++ b/Handler/Common.hs | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | {-# LANGUAGE ApplicativeDo #-} | ||
| 2 | |||
| 3 | module Handler.Common | ||
| 4 | ( inventoryListing | ||
| 5 | , itemForm | ||
| 6 | , InventoryState(..) | ||
| 7 | , FormState(..) | ||
| 8 | ) where | ||
| 9 | |||
| 10 | import Import | ||
| 11 | |||
| 12 | import Data.Unique | ||
| 13 | |||
| 14 | import qualified Data.Text as Text (pack) | ||
| 15 | |||
| 16 | import Control.Lens | ||
| 17 | |||
| 18 | import Handler.Common.Types | ||
| 19 | |||
| 20 | dayFormat :: Day -> String | ||
| 21 | dayFormat = formatTime defaultTimeLocale "%e. %b %y" | ||
| 22 | |||
| 23 | itemForm :: Maybe Item -- ^ Update existing item or insert new? | ||
| 24 | -> Html -> MForm Handler (FormResult Item, Widget) | ||
| 25 | itemForm proto identView = do | ||
| 26 | today <- utctDay <$> liftIO getCurrentTime | ||
| 27 | |||
| 28 | (kindRes, kindView) <- mreq textField "" $ itemKind <$> proto | ||
| 29 | (boughtRes, boughtWidget) <- dayForm (maybe (Just $ Just today) Just $ fmap itemBought proto) "Unknown" | ||
| 30 | (expiresRes, expiresWidget) <- dayForm (fmap itemExpires proto) "Never" | ||
| 31 | (openedRes, openedWidget) <- dayForm (fmap itemOpened proto) "Never" | ||
| 32 | |||
| 33 | let itemRes = do | ||
| 34 | itemKind <- kindRes | ||
| 35 | itemBought <- boughtRes | ||
| 36 | itemExpires <- expiresRes | ||
| 37 | itemOpened <- openedRes | ||
| 38 | return Item{ itemNormKind = normalizeKind itemKind, ..} | ||
| 39 | |||
| 40 | return . (itemRes, ) $ | ||
| 41 | [whamlet| | ||
| 42 | $newline never | ||
| 43 | #{identView} | ||
| 44 | <div .td>^{fvInput kindView} | ||
| 45 | <div .td>^{boughtWidget} | ||
| 46 | <div .td>^{expiresWidget} | ||
| 47 | <div .td>^{openedWidget} | ||
| 48 | |] | ||
| 49 | where | ||
| 50 | dayForm :: Maybe (Maybe Day) -> String -> MForm Handler (FormResult (Maybe Day), Widget) | ||
| 51 | dayForm proto label = do | ||
| 52 | today <- utctDay <$> liftIO getCurrentTime | ||
| 53 | |||
| 54 | checkboxId <- ("check" <>) . show . hashUnique <$> liftIO newUnique | ||
| 55 | |||
| 56 | (fmap (fromMaybe False) -> isNothingRes, isNothingView) <- | ||
| 57 | mopt checkBoxField ("" { fsId = Just $ Text.pack checkboxId }) . Just . Just . fromMaybe True $ fmap isNothing proto | ||
| 58 | (dayRes, dayView) <- | ||
| 59 | mreq dayField "" . Just . fromMaybe today $ join proto | ||
| 60 | |||
| 61 | let res = (bool Just (const Nothing) <$> isNothingRes) <*> dayRes | ||
| 62 | return . (res, ) $ do | ||
| 63 | [whamlet| | ||
| 64 | $newline never | ||
| 65 | <div .table> | ||
| 66 | <div .tr> | ||
| 67 | <label for=#{checkboxId} .checkbox .td> | ||
| 68 | ^{fvInput isNothingView} | ||
| 69 | <span> | ||
| 70 | #{label} | ||
| 71 | <div .tr> | ||
| 72 | <div .td .dayInput>^{fvInput dayView} | ||
| 73 | |] | ||
| 74 | |||
| 75 | inventoryListing :: InventoryState -> Widget | ||
| 76 | inventoryListing InventoryState{..} = do | ||
| 77 | setTitle "Bar Inventory" | ||
| 78 | $(widgetFile "inventoryListing") | ||
diff --git a/Handler/Common/Types.hs b/Handler/Common/Types.hs new file mode 100644 index 0000000..ca7cb8d --- /dev/null +++ b/Handler/Common/Types.hs | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | {-# LANGUAGE FunctionalDependencies #-} | ||
| 2 | |||
| 3 | module Handler.Common.Types where | ||
| 4 | |||
| 5 | import Import | ||
| 6 | |||
| 7 | import Control.Lens | ||
| 8 | |||
| 9 | data InventoryState = InventoryState | ||
| 10 | { stock :: [Entity Item] | ||
| 11 | , formState :: Maybe FormState | ||
| 12 | } | ||
| 13 | |||
| 14 | data FormState = InsertForm | ||
| 15 | { fsInsertForm :: Widget | ||
| 16 | , fsInsertEncoding :: Enctype | ||
| 17 | } | ||
| 18 | | UpdateForm | ||
| 19 | { fsUpdateItem :: ItemId | ||
| 20 | , fsUpdateForm :: Widget | ||
| 21 | , fsUpdateEncoding :: Enctype | ||
| 22 | } | ||
| 23 | makeLensesWith abbreviatedFields ''FormState | ||
diff --git a/Handler/DeleteItem.hs b/Handler/DeleteItem.hs new file mode 100644 index 0000000..ee6d9d3 --- /dev/null +++ b/Handler/DeleteItem.hs | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | module Handler.DeleteItem where | ||
| 2 | |||
| 3 | import Import | ||
| 4 | |||
| 5 | postDeleteItemR :: ItemId -> Handler TypedContent | ||
| 6 | postDeleteItemR itemId = do | ||
| 7 | runDB $ delete itemId | ||
| 8 | selectRep $ do | ||
| 9 | provideJson () | ||
| 10 | provideRep (redirect $ InventoryListingR :: Handler Html) | ||
diff --git a/Handler/InventoryListing.hs b/Handler/InventoryListing.hs new file mode 100644 index 0000000..e3c062e --- /dev/null +++ b/Handler/InventoryListing.hs | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | module Handler.InventoryListing where | ||
| 2 | |||
| 3 | import Import | ||
| 4 | import Handler.Common | ||
| 5 | |||
| 6 | getInventoryListingR, postInventoryListingR :: Handler TypedContent | ||
| 7 | getInventoryListingR = postInventoryListingR | ||
| 8 | postInventoryListingR = do | ||
| 9 | ((insertResult, fsInsertForm), fsInsertEncoding) <- runFormPost $ itemForm Nothing | ||
| 10 | |||
| 11 | mapM_ (addMessage "formError" . toHtml) =<< case insertResult of | ||
| 12 | FormSuccess newItem -> [] <$ runDB (insert newItem) | ||
| 13 | FormFailure errors -> return errors | ||
| 14 | _ -> return [] | ||
| 15 | |||
| 16 | (sortOn entityVal -> stock) <- runDB $ selectList [] [] | ||
| 17 | |||
| 18 | selectRep $ do | ||
| 19 | provideJson (stock :: [Entity Item]) | ||
| 20 | provideRep . defaultLayout $ inventoryListing InventoryState | ||
| 21 | { formState = Just InsertForm{..} | ||
| 22 | , .. | ||
| 23 | } | ||
| 24 | |||
| 25 | putInventoryListingR :: Handler Value | ||
| 26 | putInventoryListingR = returnJson =<< runDB . insertEntity =<< (requireCheckJsonBody :: Handler Item) | ||
diff --git a/Handler/Item.hs b/Handler/Item.hs new file mode 100644 index 0000000..87030bb --- /dev/null +++ b/Handler/Item.hs | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | module Handler.Item where | ||
| 2 | |||
| 3 | import Import | ||
| 4 | |||
| 5 | getItemR :: ItemId -> Handler TypedContent | ||
| 6 | getItemR itemId = do | ||
| 7 | eLookup <- runDB $ fmap (Entity itemId) <$> get itemId | ||
| 8 | case eLookup of | ||
| 9 | Nothing -> notFound | ||
| 10 | Just entity -> selectRep $ do | ||
| 11 | provideJson entity | ||
| 12 | provideRep (redirect $ InventoryListingR :#: itemId :: Handler Html) | ||
| 13 | |||
| 14 | putItemR :: ItemId -> Handler Value | ||
| 15 | putItemR itemId = do | ||
| 16 | Item{..} <- requireCheckJsonBody | ||
| 17 | returnJson . Entity itemId =<< runDB | ||
| 18 | (updateGet itemId [ ItemKind =. itemKind | ||
| 19 | , ItemNormKind =. itemNormKind | ||
| 20 | , ItemBought =. itemBought | ||
| 21 | , ItemExpires =. itemExpires | ||
| 22 | , ItemOpened =. itemOpened | ||
| 23 | ]) | ||
| 24 | |||
| 25 | patchItemR :: ItemId -> Handler Value | ||
| 26 | patchItemR itemId = do | ||
| 27 | diffs <- (requireCheckJsonBody :: Handler ItemDiffs) | ||
| 28 | returnJson . Entity itemId =<< runDB (updateGet itemId $ toUpdate diffs) | ||
| 29 | |||
| 30 | deleteItemR :: ItemId -> Handler () | ||
| 31 | deleteItemR = runDB . delete | ||
diff --git a/Handler/OpenItem.hs b/Handler/OpenItem.hs new file mode 100644 index 0000000..468c6ec --- /dev/null +++ b/Handler/OpenItem.hs | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | module Handler.OpenItem where | ||
| 2 | |||
| 3 | import Import | ||
| 4 | |||
| 5 | postOpenItemR :: ItemId -> Handler TypedContent | ||
| 6 | postOpenItemR itemId = do | ||
| 7 | today <- utctDay <$> liftIO getCurrentTime | ||
| 8 | result <- fmap (Entity itemId) . runDB $ updateGet itemId [ ItemOpened =. Just today | ||
| 9 | ] | ||
| 10 | selectRep $ do | ||
| 11 | provideJson result | ||
| 12 | provideRep (redirect $ InventoryListingR :#: itemId :: Handler Html) | ||
diff --git a/Handler/UpdateItem.hs b/Handler/UpdateItem.hs new file mode 100644 index 0000000..353572b --- /dev/null +++ b/Handler/UpdateItem.hs | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | module Handler.UpdateItem where | ||
| 2 | |||
| 3 | import Import | ||
| 4 | |||
| 5 | import Handler.Common | ||
| 6 | |||
| 7 | getUpdateItemR, postUpdateItemR :: ItemId -> Handler TypedContent | ||
| 8 | getUpdateItemR = postUpdateItemR | ||
| 9 | postUpdateItemR fsUpdateItem = do | ||
| 10 | Just entity <- fmap (Entity fsUpdateItem) <$> runDB (get fsUpdateItem) | ||
| 11 | |||
| 12 | ((updateResult, fsUpdateForm), fsUpdateEncoding) <- runFormPost . itemForm . Just $ entityVal entity | ||
| 13 | |||
| 14 | mapM_ (addMessage "formError" . toHtml) =<< case updateResult of | ||
| 15 | FormSuccess Item{..} -> [] <$ runDB (update fsUpdateItem [ ItemKind =. itemKind | ||
| 16 | , ItemNormKind =. normalizeKind itemKind | ||
| 17 | , ItemBought =. itemBought | ||
| 18 | , ItemExpires =. itemExpires | ||
| 19 | , ItemOpened =. itemOpened | ||
| 20 | ]) | ||
| 21 | FormFailure errors -> return errors | ||
| 22 | _ -> return [] | ||
| 23 | |||
| 24 | selectRep $ do | ||
| 25 | provideRep $ case updateResult of | ||
| 26 | FormSuccess _ -> redirect $ InventoryListingR :#: fsUpdateItem :: Handler Html | ||
| 27 | _ -> do | ||
| 28 | (sortOn entityVal -> stock) <- runDB $ selectList [] [] | ||
| 29 | defaultLayout $ inventoryListing InventoryState | ||
| 30 | { formState = Just UpdateForm{..} | ||
| 31 | , .. | ||
| 32 | } | ||
| 33 | provideJson () | ||
diff --git a/Import.hs b/Import.hs new file mode 100644 index 0000000..a102001 --- /dev/null +++ b/Import.hs | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | module Import | ||
| 2 | ( module Import | ||
| 3 | ) where | ||
| 4 | |||
| 5 | import Foundation as Import | ||
| 6 | import Import.NoFoundation as Import | ||
diff --git a/Import/NoFoundation.hs b/Import/NoFoundation.hs new file mode 100644 index 0000000..6872d0a --- /dev/null +++ b/Import/NoFoundation.hs | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | {-# LANGUAGE CPP #-} | ||
| 2 | module Import.NoFoundation | ||
| 3 | ( module Import | ||
| 4 | ) where | ||
| 5 | |||
| 6 | import ClassyPrelude.Yesod as Import | ||
| 7 | import Model as Import | ||
| 8 | import Settings as Import | ||
| 9 | import Settings.StaticFiles as Import | ||
| 10 | import Yesod.Auth as Import | ||
| 11 | import Yesod.Core.Types as Import (loggerSet) | ||
| 12 | import Yesod.Default.Config2 as Import | ||
| 13 | import Yesod.EmbeddedStatic as Import | ||
diff --git a/Model.hs b/Model.hs new file mode 100644 index 0000000..7b33f6e --- /dev/null +++ b/Model.hs | |||
| @@ -0,0 +1,96 @@ | |||
| 1 | {-# LANGUAGE FlexibleInstances #-} | ||
| 2 | |||
| 3 | module Model where | ||
| 4 | |||
| 5 | import ClassyPrelude.Yesod | ||
| 6 | import Database.Persist.Quasi | ||
| 7 | |||
| 8 | import Control.Monad.Writer | ||
| 9 | |||
| 10 | import Data.Text (Text) | ||
| 11 | import qualified Data.Text as Text | ||
| 12 | |||
| 13 | import Data.Aeson | ||
| 14 | |||
| 15 | -- You can define all of your database entities in the entities file. | ||
| 16 | -- You can find more information on persistent and how to declare entities | ||
| 17 | -- at: | ||
| 18 | -- http://www.yesodweb.com/book/persistent/ | ||
| 19 | share [mkPersist sqlSettings, mkMigrate "migrateAll"] | ||
| 20 | $(persistFileWith lowerCaseSettings "config/models") | ||
| 21 | |||
| 22 | instance Ord Item where | ||
| 23 | x `compare` y = mconcat | ||
| 24 | [ (isNothing $ itemOpened x) `compare` (isNothing $ itemOpened y) | ||
| 25 | , itemOpened x `compare` itemOpened y | ||
| 26 | , (isNothing $ itemExpires x) `compare` (isNothing $ itemExpires y) | ||
| 27 | , itemExpires x `compare` itemExpires x | ||
| 28 | , itemKind x `compare` itemKind x | ||
| 29 | , itemBought x `compare` itemBought x | ||
| 30 | ] | ||
| 31 | |||
| 32 | instance ToJSON Item where | ||
| 33 | toJSON Item{..} = object $ | ||
| 34 | [ "kind" .= itemKind | ||
| 35 | ] ++ maybe [] (\x -> ["bought" .= x]) itemBought | ||
| 36 | ++ maybe [] (\x -> ["expires" .= x]) itemExpires | ||
| 37 | ++ maybe [] (\x -> ["opened" .= x]) itemOpened | ||
| 38 | |||
| 39 | instance FromJSON Item where | ||
| 40 | parseJSON = withObject "Item" $ \obj -> do | ||
| 41 | itemKind <- obj .: "kind" | ||
| 42 | let | ||
| 43 | itemNormKind = normalizeKind itemKind | ||
| 44 | itemBought <- obj .:? "bought" | ||
| 45 | itemExpires <- obj .:? "expires" | ||
| 46 | itemOpened <- obj .:? "opened" | ||
| 47 | return Item{..} | ||
| 48 | |||
| 49 | instance ToJSON (Entity Item) where | ||
| 50 | toJSON = entityIdToJSON | ||
| 51 | |||
| 52 | instance FromJSON (Entity Item) where | ||
| 53 | parseJSON = entityIdFromJSON | ||
| 54 | |||
| 55 | instance ToJSON Reference where | ||
| 56 | toJSON Reference{..} = String referenceKind | ||
| 57 | |||
| 58 | instance FromJSON Reference where | ||
| 59 | parseJSON = withText "Reference" $ \referenceKind -> do | ||
| 60 | let | ||
| 61 | referenceNormKind = normalizeKind referenceKind | ||
| 62 | return Reference{..} | ||
| 63 | |||
| 64 | instance ToJSON (Entity Reference) where | ||
| 65 | toJSON = keyValueEntityToJSON | ||
| 66 | |||
| 67 | instance FromJSON (Entity Reference) where | ||
| 68 | parseJSON = keyValueEntityFromJSON | ||
| 69 | |||
| 70 | normalizeKind :: Text -> Text | ||
| 71 | normalizeKind = Text.strip . Text.toCaseFold | ||
| 72 | |||
| 73 | data ItemDiff = DiffKind Text | ||
| 74 | | DiffBought (Maybe Day) | ||
| 75 | | DiffExpires (Maybe Day) | ||
| 76 | | DiffOpened (Maybe Day) | ||
| 77 | |||
| 78 | newtype ItemDiffs = ItemDiffs [ItemDiff] | ||
| 79 | |||
| 80 | instance FromJSON ItemDiffs where | ||
| 81 | parseJSON = withObject "ItemDiff" $ \obj -> fmap ItemDiffs . execWriterT $ do | ||
| 82 | tell =<< maybe [] (pure . DiffKind) <$> lift (obj .:? "kind") | ||
| 83 | tell =<< maybe [] (pure . DiffBought) <$> lift (obj .:! "bought") | ||
| 84 | tell =<< maybe [] (pure . DiffExpires) <$> lift (obj .:! "expires") | ||
| 85 | tell =<< maybe [] (pure . DiffOpened) <$> lift (obj .:! "opened") | ||
| 86 | |||
| 87 | toUpdate :: ItemDiffs -> [Update Item] | ||
| 88 | toUpdate (ItemDiffs ds) = do | ||
| 89 | x <- ds | ||
| 90 | case x of | ||
| 91 | DiffKind t -> [ ItemKind =. t | ||
| 92 | , ItemNormKind =. normalizeKind t | ||
| 93 | ] | ||
| 94 | DiffBought d -> [ ItemBought =. d ] | ||
| 95 | DiffExpires d -> [ ItemExpires =. d ] | ||
| 96 | DiffOpened d -> [ ItemOpened =. d ] | ||
diff --git a/Settings.hs b/Settings.hs new file mode 100644 index 0000000..76aa2f3 --- /dev/null +++ b/Settings.hs | |||
| @@ -0,0 +1,107 @@ | |||
| 1 | {-# Language CPP #-} | ||
| 2 | -- | Settings are centralized, as much as possible, into this file. This | ||
| 3 | -- includes database connection settings, static file locations, etc. | ||
| 4 | -- In addition, you can configure a number of different aspects of Yesod | ||
| 5 | -- by overriding methods in the Yesod typeclass. That instance is | ||
| 6 | -- declared in the Foundation.hs file. | ||
| 7 | module Settings where | ||
| 8 | |||
| 9 | import ClassyPrelude.Yesod | ||
| 10 | import qualified Control.Exception as Exception | ||
| 11 | import Data.Aeson (Result (..), fromJSON, withObject, (.!=), | ||
| 12 | (.:?)) | ||
| 13 | import Data.FileEmbed (embedFile) | ||
| 14 | import Data.Yaml (decodeEither') | ||
| 15 | import Database.Persist.Postgresql (PostgresConf) | ||
| 16 | import Language.Haskell.TH.Syntax (Exp, Q) | ||
| 17 | import Network.Wai.Handler.Warp (HostPreference) | ||
| 18 | import Yesod.Default.Config2 (applyEnvValue, configSettingsYml) | ||
| 19 | import Yesod.Default.Util (WidgetFileSettings, widgetFileNoReload, | ||
| 20 | widgetFileReload) | ||
| 21 | |||
| 22 | #ifdef DEVELOPMENT | ||
| 23 | #define DEV_BOOL True | ||
| 24 | #else | ||
| 25 | #define DEV_BOOL False | ||
| 26 | #endif | ||
| 27 | |||
| 28 | -- | Runtime settings to configure this application. These settings can be | ||
| 29 | -- loaded from various sources: defaults, environment variables, config files, | ||
| 30 | -- theoretically even a database. | ||
| 31 | data AppSettings = AppSettings | ||
| 32 | { appStaticDir :: String | ||
| 33 | -- ^ Directory from which to serve static files. | ||
| 34 | , appDatabaseConf :: PostgresConf | ||
| 35 | -- ^ Configuration settings for accessing the database. | ||
| 36 | , appRoot :: Maybe Text | ||
| 37 | -- ^ Base for all generated URLs. If @Nothing@, determined | ||
| 38 | -- from the request headers. | ||
| 39 | , appHost :: HostPreference | ||
| 40 | -- ^ Host/interface the server should bind to. | ||
| 41 | , appPort :: Int | ||
| 42 | -- ^ Port to listen on | ||
| 43 | , appIpFromHeader :: Bool | ||
| 44 | -- ^ Get the IP address from the header when logging. Useful when sitting | ||
| 45 | -- behind a reverse proxy. | ||
| 46 | |||
| 47 | , appDetailedRequestLogging :: Bool | ||
| 48 | -- ^ Use detailed request logging system | ||
| 49 | , appShouldLogAll :: Bool | ||
| 50 | -- ^ Should all log messages be displayed? | ||
| 51 | , appReloadTemplates :: Bool | ||
| 52 | -- ^ Use the reload version of templates | ||
| 53 | } | ||
| 54 | |||
| 55 | instance FromJSON AppSettings where | ||
| 56 | parseJSON = withObject "AppSettings" $ \o -> do | ||
| 57 | let defaultDev = DEV_BOOL | ||
| 58 | appStaticDir <- o .: "static-dir" | ||
| 59 | appDatabaseConf <- o .: "database" | ||
| 60 | appRoot <- o .:? "approot" | ||
| 61 | appHost <- fromString <$> o .: "host" | ||
| 62 | appPort <- o .: "port" | ||
| 63 | appIpFromHeader <- o .: "ip-from-header" | ||
| 64 | |||
| 65 | appDetailedRequestLogging <- o .:? "detailed-logging" .!= defaultDev | ||
| 66 | appShouldLogAll <- o .:? "should-log-all" .!= defaultDev | ||
| 67 | appReloadTemplates <- o .:? "reload-templates" .!= defaultDev | ||
| 68 | |||
| 69 | return AppSettings {..} | ||
| 70 | |||
| 71 | -- | Settings for 'widgetFile', such as which template languages to support and | ||
| 72 | -- default Hamlet settings. | ||
| 73 | -- | ||
| 74 | -- For more information on modifying behavior, see: | ||
| 75 | -- | ||
| 76 | -- https://github.com/yesodweb/yesod/wiki/Overriding-widgetFile | ||
| 77 | widgetFileSettings :: WidgetFileSettings | ||
| 78 | widgetFileSettings = def | ||
| 79 | |||
| 80 | -- | How static files should be combined. | ||
| 81 | combineSettings :: CombineSettings | ||
| 82 | combineSettings = def | ||
| 83 | |||
| 84 | -- The rest of this file contains settings which rarely need changing by a | ||
| 85 | -- user. | ||
| 86 | |||
| 87 | widgetFile :: String -> Q Exp | ||
| 88 | widgetFile = (if appReloadTemplates compileTimeAppSettings | ||
| 89 | then widgetFileReload | ||
| 90 | else widgetFileNoReload) | ||
| 91 | widgetFileSettings | ||
| 92 | |||
| 93 | -- | Raw bytes at compile time of @config/settings.yml@ | ||
| 94 | configSettingsYmlBS :: ByteString | ||
| 95 | configSettingsYmlBS = $(embedFile configSettingsYml) | ||
| 96 | |||
| 97 | -- | @config/settings.yml@, parsed to a @Value@. | ||
| 98 | configSettingsYmlValue :: Value | ||
| 99 | configSettingsYmlValue = either Exception.throw id | ||
| 100 | $ decodeEither' configSettingsYmlBS | ||
| 101 | |||
| 102 | -- | A version of @AppSettings@ parsed at compile time from @config/settings.yml@. | ||
| 103 | compileTimeAppSettings :: AppSettings | ||
| 104 | compileTimeAppSettings = | ||
| 105 | case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of | ||
| 106 | Error e -> error e | ||
| 107 | Success settings -> settings | ||
diff --git a/Settings/StaticFiles.hs b/Settings/StaticFiles.hs new file mode 100644 index 0000000..bd29ca3 --- /dev/null +++ b/Settings/StaticFiles.hs | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | module Settings.StaticFiles where | ||
| 2 | |||
| 3 | import Settings (appStaticDir, compileTimeAppSettings) | ||
| 4 | import Yesod.EmbeddedStatic (mkEmbeddedStatic, embedDir) | ||
| 5 | import Prelude (Bool(..), ($)) | ||
| 6 | |||
| 7 | #ifdef DEVELOPMENT | ||
| 8 | #define DEV_BOOL True | ||
| 9 | #else | ||
| 10 | #define DEV_BOOL False | ||
| 11 | #endif | ||
| 12 | |||
| 13 | mkEmbeddedStatic DEV_BOOL "eStatic" [embedDir $ appStaticDir compileTimeAppSettings] | ||
| @@ -0,0 +1,3 @@ | |||
| 1 | #!/usr/bin/env zsh | ||
| 2 | |||
| 3 | gup -u bar.nix static/jquery.js static/webshim | ||
diff --git a/app/DevelMain.hs b/app/DevelMain.hs new file mode 100644 index 0000000..b327943 --- /dev/null +++ b/app/DevelMain.hs | |||
| @@ -0,0 +1,99 @@ | |||
| 1 | -- | Running your app inside GHCi. | ||
| 2 | -- | ||
| 3 | -- To start up GHCi for usage with Yesod, first make sure you are in dev mode: | ||
| 4 | -- | ||
| 5 | -- > cabal configure -fdev | ||
| 6 | -- | ||
| 7 | -- Note that @yesod devel@ automatically sets the dev flag. | ||
| 8 | -- Now launch the repl: | ||
| 9 | -- | ||
| 10 | -- > cabal repl --ghc-options="-O0 -fobject-code" | ||
| 11 | -- | ||
| 12 | -- To start your app, run: | ||
| 13 | -- | ||
| 14 | -- > :l DevelMain | ||
| 15 | -- > DevelMain.update | ||
| 16 | -- | ||
| 17 | -- You can also call @DevelMain.shutdown@ to stop the app | ||
| 18 | -- | ||
| 19 | -- You will need to add the foreign-store package to your .cabal file. | ||
| 20 | -- It is very light-weight. | ||
| 21 | -- | ||
| 22 | -- If you don't use cabal repl, you will need | ||
| 23 | -- to run the following in GHCi or to add it to | ||
| 24 | -- your .ghci file. | ||
| 25 | -- | ||
| 26 | -- :set -DDEVELOPMENT | ||
| 27 | -- | ||
| 28 | -- There is more information about this approach, | ||
| 29 | -- on the wiki: https://github.com/yesodweb/yesod/wiki/ghci | ||
| 30 | |||
| 31 | module DevelMain where | ||
| 32 | |||
| 33 | import Prelude | ||
| 34 | import Application (getApplicationRepl, shutdownApp) | ||
| 35 | |||
| 36 | import Control.Exception (finally) | ||
| 37 | import Control.Monad ((>=>)) | ||
| 38 | import Control.Concurrent | ||
| 39 | import Data.IORef | ||
| 40 | import Foreign.Store | ||
| 41 | import Network.Wai.Handler.Warp | ||
| 42 | import GHC.Word | ||
| 43 | |||
| 44 | -- | Start or restart the server. | ||
| 45 | -- newStore is from foreign-store. | ||
| 46 | -- A Store holds onto some data across ghci reloads | ||
| 47 | update :: IO () | ||
| 48 | update = do | ||
| 49 | mtidStore <- lookupStore tidStoreNum | ||
| 50 | case mtidStore of | ||
| 51 | -- no server running | ||
| 52 | Nothing -> do | ||
| 53 | done <- storeAction doneStore newEmptyMVar | ||
| 54 | tid <- start done | ||
| 55 | _ <- storeAction (Store tidStoreNum) (newIORef tid) | ||
| 56 | return () | ||
| 57 | -- server is already running | ||
| 58 | Just tidStore -> restartAppInNewThread tidStore | ||
| 59 | where | ||
| 60 | doneStore :: Store (MVar ()) | ||
| 61 | doneStore = Store 0 | ||
| 62 | |||
| 63 | -- shut the server down with killThread and wait for the done signal | ||
| 64 | restartAppInNewThread :: Store (IORef ThreadId) -> IO () | ||
| 65 | restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do | ||
| 66 | killThread tid | ||
| 67 | withStore doneStore takeMVar | ||
| 68 | readStore doneStore >>= start | ||
| 69 | |||
| 70 | |||
| 71 | -- | Start the server in a separate thread. | ||
| 72 | start :: MVar () -- ^ Written to when the thread is killed. | ||
| 73 | -> IO ThreadId | ||
| 74 | start done = do | ||
| 75 | (port, site, app) <- getApplicationRepl | ||
| 76 | forkIO (finally (runSettings (setPort port defaultSettings) app) | ||
| 77 | -- Note that this implies concurrency | ||
| 78 | -- between shutdownApp and the next app that is starting. | ||
| 79 | -- Normally this should be fine | ||
| 80 | (putMVar done () >> shutdownApp site)) | ||
| 81 | |||
| 82 | -- | kill the server | ||
| 83 | shutdown :: IO () | ||
| 84 | shutdown = do | ||
| 85 | mtidStore <- lookupStore tidStoreNum | ||
| 86 | case mtidStore of | ||
| 87 | -- no server running | ||
| 88 | Nothing -> putStrLn "no Yesod app running" | ||
| 89 | Just tidStore -> do | ||
| 90 | withStore tidStore $ readIORef >=> killThread | ||
| 91 | putStrLn "Yesod app is shutdown" | ||
| 92 | |||
| 93 | tidStoreNum :: Word32 | ||
| 94 | tidStoreNum = 1 | ||
| 95 | |||
| 96 | modifyStoredIORef :: Store (IORef a) -> (a -> IO a) -> IO () | ||
| 97 | modifyStoredIORef store f = withStore store $ \ref -> do | ||
| 98 | v <- readIORef ref | ||
| 99 | f v >>= writeIORef ref | ||
diff --git a/app/devel.hs b/app/devel.hs new file mode 100644 index 0000000..979103f --- /dev/null +++ b/app/devel.hs | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | {-# LANGUAGE PackageImports #-} | ||
| 2 | import "bar" Application (develMain) | ||
| 3 | import Prelude (IO) | ||
| 4 | |||
| 5 | main :: IO () | ||
| 6 | main = develMain | ||
diff --git a/app/main.hs b/app/main.hs new file mode 100644 index 0000000..4ffa93d --- /dev/null +++ b/app/main.hs | |||
| @@ -0,0 +1,5 @@ | |||
| 1 | import Prelude (IO) | ||
| 2 | import Application (appMain) | ||
| 3 | |||
| 4 | main :: IO () | ||
| 5 | main = appMain | ||
diff --git a/bar.cabal b/bar.cabal new file mode 100644 index 0000000..264339d --- /dev/null +++ b/bar.cabal | |||
| @@ -0,0 +1,128 @@ | |||
| 1 | name: bar | ||
| 2 | version: 0.0.0 | ||
| 3 | cabal-version: >= 1.8 | ||
| 4 | build-type: Simple | ||
| 5 | |||
| 6 | Flag dev | ||
| 7 | Description: Turn on development settings, like auto-reload templates. | ||
| 8 | Default: False | ||
| 9 | |||
| 10 | Flag library-only | ||
| 11 | Description: Build for use with "yesod devel" | ||
| 12 | Default: False | ||
| 13 | |||
| 14 | library | ||
| 15 | hs-source-dirs: ., app | ||
| 16 | exposed-modules: Application | ||
| 17 | Foundation | ||
| 18 | Import | ||
| 19 | Import.NoFoundation | ||
| 20 | Model | ||
| 21 | Settings | ||
| 22 | Settings.StaticFiles | ||
| 23 | Handler.Common | ||
| 24 | Handler.Common.Types | ||
| 25 | Handler.InventoryListing | ||
| 26 | Handler.UpdateItem | ||
| 27 | Handler.OpenItem | ||
| 28 | Handler.DeleteItem | ||
| 29 | Handler.Item | ||
| 30 | |||
| 31 | if flag(dev) || flag(library-only) | ||
| 32 | cpp-options: -DDEVELOPMENT | ||
| 33 | ghc-options: -Wall -fwarn-tabs -O0 | ||
| 34 | else | ||
| 35 | ghc-options: -Wall -fwarn-tabs -O2 | ||
| 36 | |||
| 37 | extensions: TemplateHaskell | ||
| 38 | QuasiQuotes | ||
| 39 | OverloadedStrings | ||
| 40 | NoImplicitPrelude | ||
| 41 | MultiParamTypeClasses | ||
| 42 | TypeFamilies | ||
| 43 | GADTs | ||
| 44 | GeneralizedNewtypeDeriving | ||
| 45 | FlexibleContexts | ||
| 46 | FlexibleInstances | ||
| 47 | EmptyDataDecls | ||
| 48 | NoMonomorphismRestriction | ||
| 49 | DeriveDataTypeable | ||
| 50 | ViewPatterns | ||
| 51 | TupleSections | ||
| 52 | RecordWildCards | ||
| 53 | CPP | ||
| 54 | |||
| 55 | build-depends: | ||
| 56 | -- Due to a bug in GHC 8.0.1, we block its usage | ||
| 57 | -- See: https://ghc.haskell.org/trac/ghc/ticket/12130 | ||
| 58 | base >= 4.8.2.0 && < 4.9 | ||
| 59 | || >= 4.9.1.0 && < 5 | ||
| 60 | |||
| 61 | , yesod >= 1.4.3 && < 1.5 | ||
| 62 | , yesod-core >= 1.4.30 && < 1.5 | ||
| 63 | , yesod-auth >= 1.4.0 && < 1.5 | ||
| 64 | , yesod-static >= 1.4.0.3 && < 1.6 | ||
| 65 | , yesod-form >= 1.4.0 && < 1.5 | ||
| 66 | , classy-prelude >= 0.10.2 | ||
| 67 | , classy-prelude-conduit >= 0.10.2 | ||
| 68 | -- version 1.0 had a bug in reexporting Handler, causing trouble | ||
| 69 | , classy-prelude-yesod >= 0.10.2 && < 1.0 | ||
| 70 | || >= 1.1 | ||
| 71 | , bytestring >= 0.9 && < 0.11 | ||
| 72 | , text >= 0.11 && < 2.0 | ||
| 73 | , persistent >= 2.0 && < 2.7 | ||
| 74 | , persistent-postgresql >= 2.1.1 && < 2.7 | ||
| 75 | , persistent-template >= 2.0 && < 2.7 | ||
| 76 | , template-haskell | ||
| 77 | , shakespeare >= 2.0 && < 2.1 | ||
| 78 | , hjsmin >= 0.1 && < 0.3 | ||
| 79 | , monad-control >= 0.3 && < 1.1 | ||
| 80 | , wai-extra >= 3.0 && < 3.1 | ||
| 81 | , yaml >= 0.8 && < 0.9 | ||
| 82 | , http-conduit >= 2.1 && < 2.3 | ||
| 83 | , directory >= 1.1 && < 1.4 | ||
| 84 | , warp >= 3.0 && < 3.3 | ||
| 85 | , data-default | ||
| 86 | , aeson >= 0.6 && < 1.1 | ||
| 87 | , conduit >= 1.0 && < 2.0 | ||
| 88 | , monad-logger >= 0.3 && < 0.4 | ||
| 89 | , fast-logger >= 2.2 && < 2.5 | ||
| 90 | , wai-logger >= 2.2 && < 2.4 | ||
| 91 | , file-embed | ||
| 92 | , safe | ||
| 93 | , unordered-containers | ||
| 94 | , containers | ||
| 95 | , vector | ||
| 96 | , time | ||
| 97 | , case-insensitive | ||
| 98 | , wai | ||
| 99 | , mtl | ||
| 100 | , lens | ||
| 101 | |||
| 102 | executable bar | ||
| 103 | if flag(library-only) | ||
| 104 | Buildable: False | ||
| 105 | |||
| 106 | main-is: main.hs | ||
| 107 | hs-source-dirs: app | ||
| 108 | build-depends: base, bar | ||
| 109 | |||
| 110 | ghc-options: -threaded -rtsopts -with-rtsopts=-N | ||
| 111 | |||
| 112 | extensions: TemplateHaskell | ||
| 113 | QuasiQuotes | ||
| 114 | OverloadedStrings | ||
| 115 | NoImplicitPrelude | ||
| 116 | MultiParamTypeClasses | ||
| 117 | TypeFamilies | ||
| 118 | GADTs | ||
| 119 | GeneralizedNewtypeDeriving | ||
| 120 | FlexibleContexts | ||
| 121 | FlexibleInstances | ||
| 122 | EmptyDataDecls | ||
| 123 | NoMonomorphismRestriction | ||
| 124 | DeriveDataTypeable | ||
| 125 | ViewPatterns | ||
| 126 | TupleSections | ||
| 127 | RecordWildCards | ||
| 128 | CPP | ||
| @@ -0,0 +1,30 @@ | |||
| 1 | { mkDerivation, aeson, base, bytestring, case-insensitive | ||
| 2 | , classy-prelude, classy-prelude-conduit, classy-prelude-yesod | ||
| 3 | , conduit, containers, data-default, directory, fast-logger | ||
| 4 | , file-embed, hjsmin, http-conduit, lens, monad-control | ||
| 5 | , monad-logger, mtl, persistent, persistent-postgresql | ||
| 6 | , persistent-template, safe, shakespeare, stdenv, template-haskell | ||
| 7 | , text, time, unordered-containers, vector, wai, wai-extra | ||
| 8 | , wai-logger, warp, yaml, yesod, yesod-auth, yesod-core, yesod-form | ||
| 9 | , yesod-static | ||
| 10 | }: | ||
| 11 | mkDerivation { | ||
| 12 | pname = "bar"; | ||
| 13 | version = "0.0.0"; | ||
| 14 | src = ./.; | ||
| 15 | isLibrary = true; | ||
| 16 | isExecutable = true; | ||
| 17 | libraryHaskellDepends = [ | ||
| 18 | aeson base bytestring case-insensitive classy-prelude | ||
| 19 | classy-prelude-conduit classy-prelude-yesod conduit containers | ||
| 20 | data-default directory fast-logger file-embed hjsmin http-conduit | ||
| 21 | lens monad-control monad-logger mtl persistent | ||
| 22 | persistent-postgresql persistent-template safe shakespeare | ||
| 23 | template-haskell text time unordered-containers vector wai | ||
| 24 | wai-extra wai-logger warp yaml yesod yesod-auth yesod-core | ||
| 25 | yesod-form yesod-static | ||
| 26 | ]; | ||
| 27 | executableHaskellDepends = [ base ]; | ||
| 28 | doHaddock = false; | ||
| 29 | license = stdenv.lib.licenses.unfree; | ||
| 30 | } | ||
diff --git a/bar.nix.gup b/bar.nix.gup new file mode 100644 index 0000000..f0f9c74 --- /dev/null +++ b/bar.nix.gup | |||
| @@ -0,0 +1,5 @@ | |||
| 1 | #!/usr/bin/env zsh | ||
| 2 | |||
| 3 | gup -u ${2:r}.cabal | ||
| 4 | cd ${2:h} | ||
| 5 | cabal2nix --no-haddock ./. >! ${1} | ||
diff --git a/config/models b/config/models new file mode 100644 index 0000000..aa335df --- /dev/null +++ b/config/models | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | Item | ||
| 2 | kind Text | ||
| 3 | normKind Text | ||
| 4 | bought Day Maybe | ||
| 5 | expires Day Maybe | ||
| 6 | opened Day Maybe | ||
| 7 | deriving Show Eq | ||
| 8 | Reference | ||
| 9 | normKind Text | ||
| 10 | kind Text | ||
| 11 | UniqueKind normKind | ||
| 12 | deriving Show Eq Ord \ No newline at end of file | ||
diff --git a/config/routes b/config/routes new file mode 100644 index 0000000..54d6593 --- /dev/null +++ b/config/routes | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | /static StaticR EmbeddedStatic appStatic | ||
| 2 | |||
| 3 | / InventoryListingR GET POST PUT | ||
| 4 | /inv/#ItemId/edit UpdateItemR GET POST | ||
| 5 | /inv/#ItemId/open OpenItemR POST | ||
| 6 | /inv/#ItemId/delete DeleteItemR POST | ||
| 7 | /inv/#ItemId ItemR GET PUT PATCH DELETE | ||
diff --git a/config/settings.yml b/config/settings.yml new file mode 100644 index 0000000..fcae60c --- /dev/null +++ b/config/settings.yml | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | # Values formatted like "_env:ENV_VAR_NAME:default_value" can be overridden by the specified environment variable. | ||
| 2 | # See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables | ||
| 3 | |||
| 4 | static-dir: "_env:STATIC_DIR:static" | ||
| 5 | host: "_env:HOST:*4" # any IPv4 host | ||
| 6 | port: "_env:PORT:3000" # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line. | ||
| 7 | ip-from-header: "_env:IP_FROM_HEADER:false" | ||
| 8 | |||
| 9 | # Default behavior: determine the application root from the request headers. | ||
| 10 | # Uncomment to set an explicit approot | ||
| 11 | approot: "_env:APPROOT:" | ||
| 12 | |||
| 13 | # Optional values with the following production defaults. | ||
| 14 | # In development, they default to the inverse. | ||
| 15 | # | ||
| 16 | # detailed-logging: false | ||
| 17 | # should-log-all: false | ||
| 18 | # reload-templates: false | ||
| 19 | |||
| 20 | database: | ||
| 21 | user: "_env:PGUSER:bar" | ||
| 22 | password: "_env:PGPASS:" | ||
| 23 | host: "_env:PGHOST:" | ||
| 24 | port: "_env:PGPORT:" | ||
| 25 | database: "_env:PGDATABASE:bar" | ||
| 26 | poolsize: "_env:PGPOOLSIZE:10" | ||
diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..1e631cf --- /dev/null +++ b/default.nix | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | argumentPackages@{ ... }: | ||
| 2 | |||
| 3 | let | ||
| 4 | defaultPackages = (import <nixpkgs> {}).haskellPackages; | ||
| 5 | haskellPackages = defaultPackages // argumentPackages; | ||
| 6 | pkgs = (import <nixpkgs> {}).pkgs // argumentPackages; | ||
| 7 | |||
| 8 | webshim = with pkgs; stdenv.mkDerivation rec { | ||
| 9 | name = "webshim-${version}"; | ||
| 10 | version = "1.16.0"; | ||
| 11 | src = fetchFromGitHub { | ||
| 12 | owner = "aFarkas"; | ||
| 13 | repo = "webshim"; | ||
| 14 | rev = "1.16.0"; | ||
| 15 | sha256 = "14pk7hljqipzp0n7vpgcfxr3w4bla57cwyd7bmwmmxrm2zn62cyh"; | ||
| 16 | }; | ||
| 17 | |||
| 18 | installPhase = '' | ||
| 19 | mkdir -p $out/js | ||
| 20 | cp -r $src/js-webshim/dev/* $out/js/ | ||
| 21 | ''; | ||
| 22 | }; | ||
| 23 | in pkgs.stdenv.lib.overrideDerivation (haskellPackages.callPackage ./bar.nix {}) (oldAttrs: { | ||
| 24 | postUnpack = '' | ||
| 25 | rm -rf bar/static/jquery.js bar/static/webshim | ||
| 26 | ln -vs ${pkgs.jquery}/js/jquery.js bar/static | ||
| 27 | ln -vs ${webshim}/js bar/static/webshim | ||
| 28 | ''; | ||
| 29 | }) | ||
diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..b6595aa --- /dev/null +++ b/shell.nix | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | { nixpkgs ? import <nixpkgs> {}, compiler ? "default" }: | ||
| 2 | |||
| 3 | let | ||
| 4 | inherit (nixpkgs) pkgs; | ||
| 5 | |||
| 6 | haskellPackages = if compiler == "default" | ||
| 7 | then pkgs.haskellPackages | ||
| 8 | else pkgs.haskell.packages.${compiler}; | ||
| 9 | |||
| 10 | drv = haskellPackages.callPackage ./bar.nix {}; | ||
| 11 | in | ||
| 12 | pkgs.stdenv.lib.overrideDerivation drv.env (oldAttrs: { | ||
| 13 | buildInputs = oldAttrs.buildInputs ++ (with pkgs; [ cabal2nix gup haskellPackages.hlint haskellPackages.stack haskellPackages.yesod-bin ]); | ||
| 14 | shellHook = '' | ||
| 15 | ${oldAttrs.shellHook} | ||
| 16 | export PROMPT_INFO="${oldAttrs.name}" | ||
| 17 | ''; | ||
| 18 | }) | ||
diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..776a6b4 --- /dev/null +++ b/stack.yaml | |||
| @@ -0,0 +1,73 @@ | |||
| 1 | # This file was automatically generated by 'stack init' | ||
| 2 | # | ||
| 3 | # Some commonly used options have been documented as comments in this file. | ||
| 4 | # For advanced use and comprehensive documentation of the format, please see: | ||
| 5 | # http://docs.haskellstack.org/en/stable/yaml_configuration/ | ||
| 6 | |||
| 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. | ||
| 8 | # A snapshot resolver dictates the compiler version and the set of packages | ||
| 9 | # to be used for project dependencies. For example: | ||
| 10 | # | ||
| 11 | # resolver: lts-3.5 | ||
| 12 | # resolver: nightly-2015-09-21 | ||
| 13 | # resolver: ghc-7.10.2 | ||
| 14 | # resolver: ghcjs-0.1.0_ghc-7.10.2 | ||
| 15 | # resolver: | ||
| 16 | # name: custom-snapshot | ||
| 17 | # location: "./custom-snapshot.yaml" | ||
| 18 | resolver: lts-8.5 | ||
| 19 | |||
| 20 | # User packages to be built. | ||
| 21 | # Various formats can be used as shown in the example below. | ||
| 22 | # | ||
| 23 | # packages: | ||
| 24 | # - some-directory | ||
| 25 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz | ||
| 26 | # - location: | ||
| 27 | # git: https://github.com/commercialhaskell/stack.git | ||
| 28 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a | ||
| 29 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a | ||
| 30 | # extra-dep: true | ||
| 31 | # subdirs: | ||
| 32 | # - auto-update | ||
| 33 | # - wai | ||
| 34 | # | ||
| 35 | # A package marked 'extra-dep: true' will only be built if demanded by a | ||
| 36 | # non-dependency (i.e. a user package), and its test suites and benchmarks | ||
| 37 | # will not be run. This is useful for tweaking upstream packages. | ||
| 38 | packages: | ||
| 39 | - '.' | ||
| 40 | # Dependency packages to be pulled from upstream that are not in the resolver | ||
| 41 | # (e.g., acme-missiles-0.3) | ||
| 42 | extra-deps: [] | ||
| 43 | |||
| 44 | # Override default flag values for local packages and extra-deps | ||
| 45 | flags: {} | ||
| 46 | |||
| 47 | # Extra package databases containing global packages | ||
| 48 | extra-package-dbs: [] | ||
| 49 | |||
| 50 | # Control whether we use the GHC we find on the path | ||
| 51 | system-ghc: true | ||
| 52 | # | ||
| 53 | # Require a specific version of stack, using version ranges | ||
| 54 | # require-stack-version: -any # Default | ||
| 55 | # require-stack-version: ">=1.3" | ||
| 56 | # | ||
| 57 | # Override the architecture used by stack, especially useful on Windows | ||
| 58 | # arch: i386 | ||
| 59 | # arch: x86_64 | ||
| 60 | # | ||
| 61 | # Extra directories used by stack for building | ||
| 62 | # extra-include-dirs: [/path/to/dir] | ||
| 63 | # extra-lib-dirs: [/path/to/dir] | ||
| 64 | # | ||
| 65 | # Allow a newer minor version of GHC than the snapshot specifies | ||
| 66 | # compiler-check: newer-minor | ||
| 67 | nix: | ||
| 68 | #enable: true | ||
| 69 | packages: | ||
| 70 | - postgresql | ||
| 71 | - zlib | ||
| 72 | - haskellPackages.yesod-bin | ||
| 73 | - haskellPackages.stack | ||
diff --git a/static/jquery.js.gup b/static/jquery.js.gup new file mode 100644 index 0000000..b6db680 --- /dev/null +++ b/static/jquery.js.gup | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | #!/usr/bin/env zsh | ||
| 2 | |||
| 3 | curl -Ls "https://cdn.jsdelivr.net/jquery/3.1.1/jquery.js" >$1 \ No newline at end of file | ||
diff --git a/static/webshim.gup b/static/webshim.gup new file mode 100644 index 0000000..f9e205c --- /dev/null +++ b/static/webshim.gup | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | #!/usr/bin/env zsh | ||
| 2 | |||
| 3 | mkdir -p ${1} | ||
| 4 | |||
| 5 | version="1.16.0" | ||
| 6 | curl -Ls "https://github.com/aFarkas/webshim/archive/${version}.tar.gz" | tar -C ${1} -xz --strip-components=3 webshim-${version}/js-webshim/dev/ | ||
diff --git a/templates/default-layout-wrapper.hamlet b/templates/default-layout-wrapper.hamlet new file mode 100644 index 0000000..dd317f1 --- /dev/null +++ b/templates/default-layout-wrapper.hamlet | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | $newline never | ||
| 2 | $doctype 5 | ||
| 3 | <html lang="en"> | ||
| 4 | <head> | ||
| 5 | <meta charset="UTF-8"> | ||
| 6 | |||
| 7 | <title>#{pageTitle pc} | ||
| 8 | <meta name="description" content=""> | ||
| 9 | <meta name="author" content=""> | ||
| 10 | |||
| 11 | <meta name="viewport" content="width=device-width,initial-scale=1"> | ||
| 12 | |||
| 13 | ^{pageHead pc} | ||
| 14 | <body> | ||
| 15 | ^{pageBody pc} | ||
diff --git a/templates/default-layout.cassius b/templates/default-layout.cassius new file mode 100644 index 0000000..492cde8 --- /dev/null +++ b/templates/default-layout.cassius | |||
| @@ -0,0 +1,77 @@ | |||
| 1 | .table | ||
| 2 | display: table | ||
| 3 | .table div | ||
| 4 | vertical-align: middle | ||
| 5 | .td | ||
| 6 | display: table-cell | ||
| 7 | text-align: center | ||
| 8 | padding: 0.25em | ||
| 9 | .tr | ||
| 10 | display: table-row | ||
| 11 | .tc | ||
| 12 | display: table-caption | ||
| 13 | padding: 0.25em | ||
| 14 | .th | ||
| 15 | display: table-cell | ||
| 16 | font-variant: small-caps | ||
| 17 | font-weight: bold | ||
| 18 | text-align: center | ||
| 19 | padding: 0.25em | ||
| 20 | .kind | ||
| 21 | display: table-cell | ||
| 22 | text-align: left | ||
| 23 | padding: 0.25em | ||
| 24 | .table .table .td, .table .table .tc, .table .table .th, .table .table .kind | ||
| 25 | padding: 0 | ||
| 26 | .error | ||
| 27 | background-color: #fdd | ||
| 28 | text-align: center | ||
| 29 | color: #c00 | ||
| 30 | list-style-type: none | ||
| 31 | button | ||
| 32 | width: 6em | ||
| 33 | display:inline-text | ||
| 34 | .day hr | ||
| 35 | width: 2em | ||
| 36 | border: 1px solid #ddd | ||
| 37 | border-style: solid none solid none | ||
| 38 | .sepBelow > div, .sepAbove > div | ||
| 39 | border: 2px none #ddd | ||
| 40 | .sepBelow > div | ||
| 41 | border-bottom-style: solid | ||
| 42 | .sepAbove > div | ||
| 43 | border-top-style: solid | ||
| 44 | .color:nth-child(even) | ||
| 45 | background-color: #f0f0f0 | ||
| 46 | .color:nth-child(odd) | ||
| 47 | background-color: #fff | ||
| 48 | body > div | ||
| 49 | margin: 0 auto | ||
| 50 | .table > h1 | ||
| 51 | display: table-caption | ||
| 52 | nav ul | ||
| 53 | display:block | ||
| 54 | text-align: center | ||
| 55 | li | ||
| 56 | display:inline-block | ||
| 57 | font-variant: small-caps | ||
| 58 | font-size: 1.5em | ||
| 59 | font-weight: bold | ||
| 60 | a | ||
| 61 | text-decoration:none | ||
| 62 | color:#aaa | ||
| 63 | a:hover | ||
| 64 | color:inherit | ||
| 65 | li.active | ||
| 66 | a | ||
| 67 | color:inherit | ||
| 68 | li::before | ||
| 69 | content:" | " | ||
| 70 | color: #ddd | ||
| 71 | li:first-child::before | ||
| 72 | content:"" | ||
| 73 | label.checkbox | ||
| 74 | input | ||
| 75 | vertical-align: middle | ||
| 76 | span | ||
| 77 | vertical-align: middle | ||
diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet new file mode 100644 index 0000000..14c8cd5 --- /dev/null +++ b/templates/default-layout.hamlet | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <nav> | ||
| 2 | <ul> | ||
| 3 | $forall MenuItem{..} <- menuItems | ||
| 4 | <li :Just menuItemRoute == mCurrentRoute:.active> | ||
| 5 | <a href=@{menuItemRoute}>#{menuItemLabel} | ||
| 6 | |||
| 7 | $if not $ null msgs | ||
| 8 | <ul #messages> | ||
| 9 | $forall (status, msg) <- msgs | ||
| 10 | <li .#{status}>#{msg} | ||
| 11 | |||
| 12 | ^{widget} | ||
diff --git a/templates/default-layout.julius b/templates/default-layout.julius new file mode 100644 index 0000000..5ef6b42 --- /dev/null +++ b/templates/default-layout.julius | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | webshims.setOptions("forms-ext", { | ||
| 2 | "widgets": { | ||
| 3 | "classes": "hide-dropdownbtn" | ||
| 4 | } | ||
| 5 | }); | ||
| 6 | webshims.activeLang("en-GB"); | ||
| 7 | webshims.polyfill("forms forms-ext"); | ||
diff --git a/templates/default-message-widget.hamlet b/templates/default-message-widget.hamlet new file mode 100644 index 0000000..2eee196 --- /dev/null +++ b/templates/default-message-widget.hamlet | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | <div .container> | ||
| 2 | <div .row> | ||
| 3 | <div .col-md-2> | ||
| 4 | <div .col-md-8> | ||
| 5 | <h1>#{title} | ||
| 6 | ^{body} | ||
diff --git a/templates/homepage.hamlet b/templates/homepage.hamlet new file mode 100644 index 0000000..fb79965 --- /dev/null +++ b/templates/homepage.hamlet | |||
| @@ -0,0 +1,139 @@ | |||
| 1 | <div .masthead> | ||
| 2 | <div .container> | ||
| 3 | <div .row> | ||
| 4 | <h1 .header> | ||
| 5 | Yesod—a modern framework for blazing fast websites | ||
| 6 | <h2> | ||
| 7 | Fast, stable & spiced with great community | ||
| 8 | <a href="http://www.yesodweb.com/book/" .btn.btn-info.btn-lg> | ||
| 9 | Read the Book | ||
| 10 | |||
| 11 | <div .container> | ||
| 12 | <!-- Starting | ||
| 13 | ================================================== --> | ||
| 14 | <div .bs-docs-section> | ||
| 15 | <div .row> | ||
| 16 | <div .col-lg-12> | ||
| 17 | <div .page-header> | ||
| 18 | <h1 #start>Starting | ||
| 19 | |||
| 20 | <p> | ||
| 21 | Now that you have a working project you should use the | ||
| 22 | <a href=http://www.yesodweb.com/book/>Yesod book</a> to learn more. | ||
| 23 | <p> | ||
| 24 | You can also use this scaffolded site to explore some concepts, and best practices. | ||
| 25 | |||
| 26 | <ul .list-group> | ||
| 27 | |||
| 28 | <li .list-group-item> | ||
| 29 | This page was generated by the <tt>#{handlerName}</tt> handler in | ||
| 30 | <tt>Handler/Home.hs</tt>. | ||
| 31 | |||
| 32 | <li .list-group-item> | ||
| 33 | The <tt>#{handlerName}</tt> handler is set to generate your | ||
| 34 | site's home screen in Routes file | ||
| 35 | <tt>config/routes | ||
| 36 | |||
| 37 | <li .list-group-item> | ||
| 38 | We can link to other handlers, like the <a href="@{ProfileR}">Profile</a>. | ||
| 39 | Try it out as an anonymous user and see the access denied. | ||
| 40 | Then, try to <a href="@{AuthR LoginR}">login</a> with the dummy authentication added | ||
| 41 | while in development. | ||
| 42 | |||
| 43 | <li .list-group-item> | ||
| 44 | The HTML you are seeing now is actually composed by a number of <em>widgets</em>, # | ||
| 45 | most of them are brought together by the <tt>defaultLayout</tt> function which # | ||
| 46 | is defined in the <tt>Foundation.hs</tt> module, and used by <tt>#{handlerName}</tt>. # | ||
| 47 | All the files for templates and wigdets are in <tt>templates</tt>. | ||
| 48 | |||
| 49 | <li .list-group-item> | ||
| 50 | A Widget's Html, Css and Javascript are separated in three files with the | ||
| 51 | <tt>.hamlet</tt>, <tt>.lucius</tt> and <tt>.julius</tt> extensions. | ||
| 52 | |||
| 53 | <li .list-group-item ##{aDomId}> | ||
| 54 | If you had javascript enabled then you wouldn't be seeing this. | ||
| 55 | |||
| 56 | <hr> | ||
| 57 | |||
| 58 | <!-- Forms | ||
| 59 | ================================================== --> | ||
| 60 | <div .bs-docs-section> | ||
| 61 | <div .row> | ||
| 62 | <div .col-lg-12> | ||
| 63 | <div .page-header> | ||
| 64 | <h1 #forms>Forms | ||
| 65 | |||
| 66 | <p> | ||
| 67 | This is an example of a form. Read the | ||
| 68 | <a href="http://www.yesodweb.com/book/forms">Forms chapter</a> # | ||
| 69 | on the yesod book to learn more about them. | ||
| 70 | |||
| 71 | <div .row> | ||
| 72 | <div .col-lg-6> | ||
| 73 | <div .bs-callout bs-callout-info well> | ||
| 74 | <form .form-horizontal method=post action=@{HomeR}#forms enctype=#{formEnctype}> | ||
| 75 | ^{formWidget} | ||
| 76 | |||
| 77 | <button .btn.btn-primary type="submit"> | ||
| 78 | Upload it! | ||
| 79 | |||
| 80 | |||
| 81 | <div .col-lg-4.col-lg-offset-1> | ||
| 82 | <div .bs-callout.bs-callout-info.upload-response> | ||
| 83 | |||
| 84 | $maybe (FileForm info con) <- submission | ||
| 85 | Your file type is <em>#{fileContentType info}</em>. You say it has: <em>#{con}</em> | ||
| 86 | |||
| 87 | $nothing | ||
| 88 | File upload result will be here... | ||
| 89 | |||
| 90 | |||
| 91 | <hr> | ||
| 92 | |||
| 93 | <!-- JSON | ||
| 94 | ================================================== --> | ||
| 95 | <div .bs-docs-section> | ||
| 96 | <div .row> | ||
| 97 | <div .col-lg-12> | ||
| 98 | <div .page-header> | ||
| 99 | <h1 #json>JSON | ||
| 100 | |||
| 101 | <p> | ||
| 102 | Yesod has JSON support baked-in. | ||
| 103 | The form below makes an AJAX request with Javascript, | ||
| 104 | then updates the page with your submission. | ||
| 105 | (see <tt>Handler/Comment.hs</tt>, <tt>templates/homepage.julius</tt>, | ||
| 106 | and <tt>Handler/Home.hs</tt> for the implementation). | ||
| 107 | |||
| 108 | <div .row> | ||
| 109 | <div .col-lg-6> | ||
| 110 | <div .bs-callout.bs-callout-info.well> | ||
| 111 | <form .form-horizontal ##{commentFormId}> | ||
| 112 | <div .field> | ||
| 113 | <textarea rows="2" ##{commentTextareaId} placeholder="Your comment here..." required></textarea> | ||
| 114 | |||
| 115 | <button .btn.btn-primary type=submit> | ||
| 116 | Create comment | ||
| 117 | |||
| 118 | <div .col-lg-4.col-lg-offset-1> | ||
| 119 | <div .bs-callout.bs-callout-info> | ||
| 120 | <small> | ||
| 121 | Your comments will appear here. You can also open the | ||
| 122 | console log to see the raw response from the server. | ||
| 123 | <ul ##{commentListId}> | ||
| 124 | |||
| 125 | <hr> | ||
| 126 | |||
| 127 | <!-- Testing | ||
| 128 | ================================================== --> | ||
| 129 | <div .bs-docs-section> | ||
| 130 | <div .row> | ||
| 131 | <div .col-lg-12> | ||
| 132 | <div .page-header> | ||
| 133 | <h1 #test>Testing | ||
| 134 | |||
| 135 | <p> | ||
| 136 | And last but not least, Testing. In <tt>test/Spec.hs</tt> you will find a # | ||
| 137 | test suite that performs tests on this page. | ||
| 138 | <p> | ||
| 139 | You can run your tests by doing: <code>stack test</code> | ||
diff --git a/templates/homepage.julius b/templates/homepage.julius new file mode 100644 index 0000000..865882e --- /dev/null +++ b/templates/homepage.julius | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | document.getElementById(#{toJSON aDomId}).innerHTML = "This text was added by the Javascript part of the homepage widget."; | ||
| 2 | |||
| 3 | $(function() { | ||
| 4 | $("##{rawJS commentFormId}").submit(function(event) { | ||
| 5 | event.preventDefault(); | ||
| 6 | |||
| 7 | var message = $("##{rawJS commentTextareaId}").val(); | ||
| 8 | // (Browsers that enforce the "required" attribute on the textarea won't see this alert) | ||
| 9 | if (!message) { | ||
| 10 | alert("Please fill out the comment form first."); | ||
| 11 | return; | ||
| 12 | } | ||
| 13 | |||
| 14 | // Make an AJAX request to the server to create a new comment | ||
| 15 | $.ajax({ | ||
| 16 | url: '@{CommentR}', | ||
| 17 | type: 'POST', | ||
| 18 | contentType: "application/json", | ||
| 19 | data: JSON.stringify({ | ||
| 20 | message: message, | ||
| 21 | }), | ||
| 22 | success: function (data) { | ||
| 23 | var newNode = $("<li></li>"); | ||
| 24 | newNode.text(data.message); | ||
| 25 | console.log(data); | ||
| 26 | $("##{rawJS commentListId}").append(newNode); | ||
| 27 | }, | ||
| 28 | error: function (data) { | ||
| 29 | console.log("Error creating comment: " + data); | ||
| 30 | }, | ||
| 31 | }); | ||
| 32 | |||
| 33 | }); | ||
| 34 | }); | ||
diff --git a/templates/homepage.lucius b/templates/homepage.lucius new file mode 100644 index 0000000..3197fd5 --- /dev/null +++ b/templates/homepage.lucius | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | h2##{aDomId} { | ||
| 2 | color: #990 | ||
| 3 | } | ||
| 4 | |||
| 5 | li { | ||
| 6 | line-height: 2em; | ||
| 7 | font-size: 16px | ||
| 8 | } | ||
| 9 | |||
| 10 | ##{commentTextareaId} { | ||
| 11 | width: 400px; | ||
| 12 | height: 100px; | ||
| 13 | } | ||
diff --git a/templates/inventoryListing.hamlet b/templates/inventoryListing.hamlet new file mode 100644 index 0000000..7c2c06b --- /dev/null +++ b/templates/inventoryListing.hamlet | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | <div .table> | ||
| 2 | <div .tr .sepBelow> | ||
| 3 | <div .th>Description | ||
| 4 | <div .th>Bought | ||
| 5 | <div .th>Expires | ||
| 6 | <div .th>Opened | ||
| 7 | <div .th>Actions | ||
| 8 | $if isJust (preview insertForm =<< formState) | ||
| 9 | $with Just InsertForm{..} <- formState | ||
| 10 | <form .tr .sepBelow action=@{InventoryListingR} method=post enctype=#{fsInsertEncoding}> | ||
| 11 | ^{fsInsertForm} | ||
| 12 | <div .td> | ||
| 13 | <button type=submit> | ||
| 14 | Insert | ||
| 15 | $forall Entity itemId Item{..} <- stock | ||
| 16 | $if Just itemId == (preview updateItem =<< formState) | ||
| 17 | $with Just UpdateForm{..} <- formState | ||
| 18 | <form .tr .color action=@{UpdateItemR fsUpdateItem}##{toPathPiece fsUpdateItem} method=post enctype=#{fsUpdateEncoding} ##{toPathPiece fsUpdateItem}> | ||
| 19 | ^{fsUpdateForm} | ||
| 20 | <div .td> | ||
| 21 | <button type=submit> | ||
| 22 | Save Changes | ||
| 23 | $else | ||
| 24 | <div .tr .color ##{toPathPiece itemId}> | ||
| 25 | <div .kind>#{itemKind} | ||
| 26 | <div .td .day> | ||
| 27 | $maybe bought <- itemBought | ||
| 28 | #{dayFormat bought} | ||
| 29 | $nothing | ||
| 30 | <hr> | ||
| 31 | <div .td .day> | ||
| 32 | $maybe expires <- itemExpires | ||
| 33 | #{dayFormat expires} | ||
| 34 | $nothing | ||
| 35 | <hr> | ||
| 36 | <div .td .day> | ||
| 37 | $maybe opened <- itemOpened | ||
| 38 | #{dayFormat opened} | ||
| 39 | $nothing | ||
| 40 | <form method=post action=@{OpenItemR itemId}> | ||
| 41 | <button type=submit> | ||
| 42 | Open | ||
| 43 | <div .td> | ||
| 44 | <form method=get action=@{UpdateItemR itemId}##{toPathPiece itemId}> | ||
| 45 | <button type=submit> | ||
| 46 | Edit | ||
| 47 | <form method=post action=@{DeleteItemR itemId}> | ||
| 48 | <button type=submit> | ||
| 49 | Delete | ||
diff --git a/templates/profile.hamlet b/templates/profile.hamlet new file mode 100644 index 0000000..2420de6 --- /dev/null +++ b/templates/profile.hamlet | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | <div .ui.container> | ||
| 2 | |||
| 3 | <h1> | ||
| 4 | Access granted! | ||
| 5 | |||
| 6 | <p> | ||
| 7 | This page is protected and access is allowed only for authenticated users. | ||
| 8 | |||
| 9 | <p> | ||
| 10 | Your data is protected with us <strong><span class="username">#{userIdent user}</span></strong>! | ||
