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