1
1
mirror of https://github.com/aelve/guide.git synced 2024-11-22 11:33:34 +03:00

Cleanup the way we work with record fields (#365)

* Use 'fields' where appropriate

* Don't use makeFields

* Rename fields of Config

* Don't use makeFields for Markdown types
This commit is contained in:
Artyom Kazak 2019-08-11 13:19:39 +03:00 committed by mergify[bot]
parent c259052b6d
commit bd51f87bed
28 changed files with 764 additions and 738 deletions

View File

@ -214,6 +214,8 @@ library
, ConstraintKinds
, InstanceSigs
, DerivingStrategies
, TemplateHaskellQuotes
, ScopedTypeVariables
test-suite tests
main-is: Main.hs

View File

@ -1,4 +1,5 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
@ -63,23 +64,23 @@ createCategory title' group' =
-- | Edit category's note.
setCategoryNotes :: Uid Category -> CTextEdit -> Guider NoContent
setCategoryNotes catId CTextEdit{..} =
setCategoryNotes catId $(fields 'CTextEdit) =
logHandler "setCategoryNotes" [attr "catId" catId] $ do
serverModified <- markdownBlockMdSource . _categoryNotes <$> getCategoryOrFail catId
serverModified <- markdownBlockSource . categoryNotes <$> getCategoryOrFail catId
checkConflict CTextEdit{..} serverModified
addEdit . fst =<< dbUpdate (SetCategoryNotes catId cteModified)
pure NoContent
-- | Edit category's info (title, group, status, sections (pro/con, ecosystem, note)).
setCategoryInfo :: Uid Category -> CCategoryInfoEdit -> Guider NoContent
setCategoryInfo catId CCategoryInfoEdit{..} =
setCategoryInfo catId $(fields 'CCategoryInfoEdit) =
logHandler "setCategoryInfo" [attr "catId" catId] $ do
category <- getCategoryOrFail catId
-- TODO diff and merge
(editTitle, _) <- dbUpdate $ SetCategoryTitle catId ccieTitle
(editGroup, _) <- dbUpdate $ SetCategoryGroup catId ccieGroup
(editStatus, _) <- dbUpdate $ SetCategoryStatus catId ccieStatus
let oldEnabledSections = category ^. enabledSections
let oldEnabledSections = categoryEnabledSections category
let newEnabledSections = ccieSections
(editSection, _) <- dbUpdate $ ChangeCategoryEnabledSections catId
(newEnabledSections S.\\ oldEnabledSections)
@ -109,7 +110,7 @@ getItem itemId =
--
-- Returns the ID of the created item.
createItem :: Uid Category -> CCreateItem -> Guider (Uid Item)
createItem catId CCreateItem{..} =
createItem catId $(fields 'CCreateItem) =
logHandler "createItem" [attr "catId" catId, attr "name" cciName] $ do
_ <- getCategoryOrFail catId
when (T.null cciName) $
@ -123,7 +124,7 @@ createItem catId CCreateItem{..} =
-- | Modify item info. Fields that are not present ('Nothing') are not modified.
setItemInfo :: Uid Item -> CItemInfoEdit -> Guider NoContent
setItemInfo itemId CItemInfoEdit{..} =
setItemInfo itemId $(fields 'CItemInfoEdit) =
logHandler "setItemInfo" [attr "itemId" itemId] $ do
void $ getItemOrFail itemId
-- TODO diff and merge
@ -137,27 +138,27 @@ setItemInfo itemId CItemInfoEdit{..} =
-- | Set item's summary.
setItemSummary :: Uid Item -> CTextEdit -> Guider NoContent
setItemSummary itemId CTextEdit{..} =
setItemSummary itemId $(fields 'CTextEdit) =
logHandler "setItemSummary" [attr "itemId" itemId] $ do
serverModified <- markdownBlockMdSource . _itemSummary <$> getItemOrFail itemId
serverModified <- markdownBlockSource . itemSummary <$> getItemOrFail itemId
checkConflict CTextEdit{..} serverModified
addEdit . fst =<< dbUpdate (SetItemSummary itemId cteModified)
pure NoContent
-- | Set item's ecosystem.
setItemEcosystem :: Uid Item -> CTextEdit -> Guider NoContent
setItemEcosystem itemId CTextEdit{..} =
setItemEcosystem itemId $(fields 'CTextEdit) =
logHandler "setItemEcosystem" [attr "itemId" itemId] $ do
serverModified <- markdownBlockMdSource . _itemEcosystem <$> getItemOrFail itemId
serverModified <- markdownBlockSource . itemEcosystem <$> getItemOrFail itemId
checkConflict CTextEdit{..} serverModified
addEdit . fst =<< dbUpdate (SetItemEcosystem itemId cteModified)
pure NoContent
-- | Set item's notes.
setItemNotes :: Uid Item -> CTextEdit -> Guider NoContent
setItemNotes itemId CTextEdit{..} =
setItemNotes itemId $(fields 'CTextEdit) =
logHandler "setItemNotes" [attr "itemId" itemId] $ do
serverModified <- markdownTreeMdSource . _itemNotes <$> getItemOrFail itemId
serverModified <- markdownTreeSource . itemNotes <$> getItemOrFail itemId
checkConflict CTextEdit{..} serverModified
addEdit . fst =<< dbUpdate (SetItemNotes itemId cteModified)
pure NoContent
@ -172,7 +173,7 @@ deleteItem itemId =
-- | Move item up or down
moveItem :: Uid Item -> CMove -> Guider NoContent
moveItem itemId CMove{..} =
moveItem itemId $(fields 'CMove) =
logHandler "moveItem" [attr "itemId" itemId] $ do
void $ getItemOrFail itemId
addEdit =<< dbUpdate (MoveItem itemId (cmDirection == DirectionUp))
@ -190,7 +191,7 @@ getTrait itemId traitId =
-- | Create a trait (pro/con).
createTrait :: Uid Item -> CCreateTrait -> Guider (Uid Trait)
createTrait itemId CCreateTrait{..} =
createTrait itemId $(fields 'CCreateTrait) =
logHandler "createTrait" [attr "itemId" itemId] $ do
when (T.null cctContent) $
throwError err400{errReasonPhrase = "'content' can not be empty"}
@ -202,9 +203,9 @@ createTrait itemId CCreateTrait{..} =
-- | Update the text of a trait (pro/con).
setTrait :: Uid Item -> Uid Trait -> CTextEdit -> Guider NoContent
setTrait itemId traitId CTextEdit{..} =
setTrait itemId traitId $(fields 'CTextEdit) =
logHandler "setTrait" [attr "itemId" itemId, attr "traitId" traitId] $ do
serverModified <- markdownInlineMdSource . _traitContent <$> getTraitOrFail itemId traitId
serverModified <- markdownInlineSource . traitContent <$> getTraitOrFail itemId traitId
checkConflict CTextEdit{..} serverModified
addEdit . fst =<< dbUpdate (SetTraitContent itemId traitId cteModified)
pure NoContent
@ -219,7 +220,7 @@ deleteTrait itemId traitId =
-- | Move trait up or down
moveTrait :: Uid Item -> Uid Trait -> CMove -> Guider NoContent
moveTrait itemId traitId CMove{..} =
moveTrait itemId traitId $(fields 'CMove) =
logHandler "moveTrait" [attr "itemId" itemId, attr "traitId" traitId] $ do
void $ getTraitOrFail itemId traitId
addEdit =<< dbUpdate (MoveTrait itemId traitId (cmDirection == DirectionUp))
@ -299,7 +300,7 @@ getTraitOrFail itemId traitId = do
-- | Checker. When states of database before and after editing is different, fail with a conflict data.
checkConflict :: CTextEdit -> Text -> Guider ()
checkConflict CTextEdit{..} serverModified = do
checkConflict $(fields 'CTextEdit) serverModified = do
let original = cteOriginal
let modified = cteModified
when (original /= serverModified) $ do

View File

@ -15,7 +15,7 @@ import Imports
import Data.Swagger.Lens hiding (format)
import Network.Wai (Middleware, Request)
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..), cors, corsOrigins,
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..), corsOrigins,
simpleCorsResourcePolicy)
import Servant
import Servant.API.Generic
@ -32,6 +32,7 @@ import Guide.State
import qualified Network.Wai.Handler.Warp as Warp
import qualified Data.Acid as Acid
import qualified Network.Wai.Middleware.Cors as Cors
-- | The type that 'runApiServer' serves.
type FullApi =
@ -41,19 +42,19 @@ type FullApi =
-- | Serve the API on port 4400.
runApiServer :: Logger -> Config -> Acid.AcidState GlobalState -> IO ()
runApiServer logger Config{..} db = do
logDebugIO logger $ format "API is running on port {}" _portApi
logDebugIO logger $ format "API is running on port {}" portApi
let guideSettings = Warp.defaultSettings
& Warp.setOnException (logException logger)
& Warp.setPort _portApi
& Warp.setPort portApi
Warp.runSettings guideSettings $ corsPolicy $
serve (Proxy @FullApi) (fullServer db logger Config{..})
where
corsPolicy :: Middleware
corsPolicy =
if _cors then cors (const $ Just (policy _portApi))
else cors (const Nothing)
policy :: Int -> CorsResourcePolicy
policy portApi = simpleCorsResourcePolicy
if cors then Cors.cors (const $ Just policy)
else Cors.cors (const Nothing)
policy :: CorsResourcePolicy
policy = simpleCorsResourcePolicy
-- TODO: Add Guide's frontend address (and maybe others resources)
-- to list of `corsOrigins` to allow CORS requests
{ corsOrigins = Just (

View File

@ -1,4 +1,5 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
@ -69,7 +70,7 @@ import Guide.Api.Utils
import Guide.Markdown
import Guide.Search
import Guide.Types.Core as G
import Guide.Utils (Uid (..), Url)
import Guide.Utils (Uid (..), Url, fields)
import qualified Data.Aeson as A
import Data.Swagger as S
@ -431,7 +432,7 @@ data CCategoryInfo = CCategoryInfo
{ cciId :: Uid Category
, cciTitle :: Text
, cciCreated :: UTCTime
, cciGroup_ :: Text
, cciGroup :: Text
, cciStatus :: CategoryStatus
}
deriving (Show, Generic)
@ -454,13 +455,20 @@ instance ToSchema CCategoryInfo where
-- | Factory to create a 'CCategoryInfo' from a 'Category'
toCCategoryInfo :: Category -> CCategoryInfo
toCCategoryInfo Category{..} = CCategoryInfo
{ cciId = _categoryUid
, cciTitle = _categoryTitle
, cciCreated = _categoryCreated
, cciGroup_ = _categoryGroup_
, cciStatus = _categoryStatus
toCCategoryInfo $(fields 'Category) = CCategoryInfo
{ cciId = categoryUid
, cciTitle = categoryTitle
, cciCreated = categoryCreated
, cciGroup = categoryGroup
, cciStatus = categoryStatus
}
where
-- Ignored "heavy" fields
_ = categoryItems
_ = categoryItemsDeleted
_ = categoryNotes
-- Not heavy, just not particularly useful metadata
_ = categoryEnabledSections
----------------------------------------------------------------------------
-- CCategoryFull
@ -497,15 +505,20 @@ instance ToSchema CCategoryFull where
-- | Factory to create a 'CCategoryFull' from a 'Category'
toCCategoryFull :: Category -> CCategoryFull
toCCategoryFull Category{..} = CCategoryFull
{ ccfId = _categoryUid
, ccfTitle = _categoryTitle
, ccfGroup = _categoryGroup_
, ccfStatus = _categoryStatus
, ccfDescription = toCMarkdown _categoryNotes
, ccfSections = _categoryEnabledSections
, ccfItems = fmap toCItemFull _categoryItems
toCCategoryFull $(fields 'Category) = CCategoryFull
{ ccfId = categoryUid
, ccfTitle = categoryTitle
, ccfGroup = categoryGroup
, ccfStatus = categoryStatus
, ccfDescription = toCMarkdown categoryNotes
, ccfSections = categoryEnabledSections
, ccfItems = fmap toCItemFull categoryItems
}
where
-- Ignored fields
_ = categoryItemsDeleted
-- TODO: return creation time
_ = categoryCreated
----------------------------------------------------------------------------
-- CCategoryInfoEdit
@ -571,13 +584,20 @@ instance ToSchema CItemInfo where
-- | Factory to create a 'CItemInfo' from an 'Item'
toCItemInfo :: Item -> CItemInfo
toCItemInfo Item{..} = CItemInfo
{ ciiId = _itemUid
, ciiCreated = _itemCreated
, ciiName = _itemName
, ciiHackage = _itemHackage
, ciiLink = _itemLink
toCItemInfo $(fields 'Item) = CItemInfo
{ ciiId = itemUid
, ciiCreated = itemCreated
, ciiName = itemName
, ciiHackage = itemHackage
, ciiLink = itemLink
}
where
-- We don't return "heavy" fields
_ = itemNotes
_ = itemEcosystem
_ = itemSummary
_ = (itemCons, itemConsDeleted)
_ = (itemPros, itemProsDeleted)
----------------------------------------------------------------------------
-- CItemInfoEdit
@ -662,19 +682,22 @@ instance ToSchema CItemFull where
-- | Factory to create a 'CItemFull' from an 'Item'
toCItemFull :: Item -> CItemFull
toCItemFull Item{..} = CItemFull
{ cifId = _itemUid
, cifName = _itemName
, cifCreated = _itemCreated
, cifHackage = _itemHackage
, cifSummary = toCMarkdown _itemSummary
, cifPros = fmap toCTrait _itemPros
, cifCons = fmap toCTrait _itemCons
, cifEcosystem = toCMarkdown _itemEcosystem
, cifNotes = toCMarkdown _itemNotes
, cifLink = _itemLink
, cifToc = map toCTocHeading (markdownTreeMdTOC _itemNotes)
toCItemFull $(fields 'Item) = CItemFull
{ cifId = itemUid
, cifName = itemName
, cifCreated = itemCreated
, cifHackage = itemHackage
, cifSummary = toCMarkdown itemSummary
, cifPros = fmap toCTrait itemPros
, cifCons = fmap toCTrait itemCons
, cifEcosystem = toCMarkdown itemEcosystem
, cifNotes = toCMarkdown itemNotes
, cifLink = itemLink
, cifToc = map toCTocHeading (markdownTreeTOC itemNotes)
}
where
-- Ignored fields
_ = (itemProsDeleted, itemConsDeleted)
----------------------------------------------------------------------------
-- CTrait
@ -697,9 +720,9 @@ instance ToSchema CTrait where
-- | Factory to create a 'CTrait' from a 'Trait'
toCTrait :: Trait -> CTrait
toCTrait trait = CTrait
{ ctId = trait ^. uid
, ctContent = toCMarkdown $ trait ^. content
toCTrait $(fields 'Trait) = CTrait
{ ctId = traitUid
, ctContent = toCMarkdown traitContent
}
----------------------------------------------------------------------------
@ -730,22 +753,33 @@ instance ToSchema CMarkdown where
class ToCMarkdown md where toCMarkdown :: md -> CMarkdown
instance ToCMarkdown MarkdownInline where
toCMarkdown md = CMarkdown
{ cmdText = md^.mdSource
, cmdHtml = toText (md^.mdHtml)
toCMarkdown $(fields 'MarkdownInline) = CMarkdown
{ cmdText = markdownInlineSource
, cmdHtml = toText markdownInlineHtml
}
where
-- Ignored fields
_ = markdownInlineMarkdown
instance ToCMarkdown MarkdownBlock where
toCMarkdown md = CMarkdown
{ cmdText = md^.mdSource
, cmdHtml = toText (md^.mdHtml)
toCMarkdown $(fields 'MarkdownBlock) = CMarkdown
{ cmdText = markdownBlockSource
, cmdHtml = toText markdownBlockHtml
}
where
-- Ignored fields
_ = markdownBlockMarkdown
instance ToCMarkdown MarkdownTree where
toCMarkdown md = CMarkdown
{ cmdText = md^.mdSource
toCMarkdown md@($(fields 'MarkdownTree)) = CMarkdown
{ cmdText = markdownTreeSource
, cmdHtml = toText . renderText $ toHtml md
}
where
-- Ignored fields
_ = markdownTreeStructure
_ = markdownTreeTOC
_ = markdownTreeIdPrefix
----------------------------------------------------------------------------
-- CTocHeading
@ -773,8 +807,8 @@ instance ToSchema CTocHeading where
-- | 'toCTocHeading' converts a table of contents into the format expected by the frontend.
toCTocHeading :: Tree Heading -> CTocHeading
toCTocHeading Node{..} = CTocHeading
{ cthContent = toCMarkdown $ headingMd rootLabel
toCTocHeading $(fields 'Node) = CTocHeading
{ cthContent = toCMarkdown $ headingMarkdown rootLabel
, cthSlug = headingSlug rootLabel
, cthSubheadings = map toCTocHeading subForest
}
@ -919,13 +953,13 @@ toCSearchResult (SRCategory cat) =
-- is about).
--
-- TODO: just extract the first paragraph, not the preface.
extractPreface $ toMarkdownTree "" $ cat^.G.notes.mdSource
extractPreface $ toMarkdownTree "" $ markdownBlockSource (categoryNotes cat)
}
toCSearchResult (SRItem cat item) =
CSRItemResult $ CSRItem
{ csriCategory = toCCategoryInfo cat
, csriInfo = toCItemInfo item
, csriSummary = Just (toCMarkdown (item ^. G.summary))
, csriSummary = Just (toCMarkdown (itemSummary item))
, csriEcosystem = Nothing
}
-- TODO: currently if there are matches in both item description and item
@ -935,7 +969,7 @@ toCSearchResult (SRItemEcosystem cat item) =
{ csriCategory = toCCategoryInfo cat
, csriInfo = toCItemInfo item
, csriSummary = Nothing
, csriEcosystem = Just (toCMarkdown (item ^. ecosystem))
, csriEcosystem = Just (toCMarkdown (itemEcosystem item))
}
----------------------------------------------------------------------------

View File

@ -48,13 +48,11 @@ import qualified Data.HashMap.Strict.InsOrd as InsOrd
-- | Nice JSON options.
--
-- @ciConsDeleted@ becomes @cons_deleted@. Underscores at the end are
-- dropped (useful for fields like @categoryGroup_@).
-- @ciConsDeleted@ becomes @cons_deleted@.
jsonOptions :: Options
jsonOptions = defaultOptions{ fieldLabelModifier = camelTo2 '_' . trim }
where
trim :: String -> String
trim = dropWhileEnd (== '_') . dropWhile (not . isUpper)
jsonOptions = defaultOptions
{ fieldLabelModifier = camelTo2 '_' . dropWhile (not . isUpper)
}
-- | Swagger schema-generating options that match 'jsonOptions'.
schemaOptions :: SchemaOptions

View File

@ -1,4 +1,5 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
-- | Server config. For instance, the admin password is stored here, as well
-- as the base url (for correct link generation in feeds).
@ -31,100 +32,102 @@ import qualified Data.ByteString.Lazy as BSL
data Config = Config {
-- | URL where the site is deployed. Used for generating feeds (which
-- require absolute URLs).
_baseUrl :: Url,
baseUrl :: Url,
-- | Google site verification token. Will be inserted into all pages.
_googleToken :: Text,
googleToken :: Text,
-- | Password for the admin user.
_adminPassword :: Text,
adminPassword :: Text,
-- | Link to a place to discuss the site. Will be placed in the header
_discussLink :: Maybe Url,
discussLink :: Maybe Url,
-- | Link to Matomo to gather analytics about user actions. Format of the
-- link shoud be like <http://localhost:8081/piwik.php>.
_matomoLink :: Maybe Url,
matomoLink :: Maybe Url,
-- | Port for serving the main site (old backend and frontend).
_portMain :: Int,
portMain :: Int,
-- | Port for serving the API.
_portApi :: Int,
portApi :: Int,
-- | Port for serving EKG stats.
_portEkg :: Int,
portEkg :: Int,
-- | CORS switch on/off.
_cors :: Bool,
cors :: Bool,
-- | EKG switch on/off.
_ekg :: Bool,
ekg :: Bool,
-- | Whether to log to @stderr@.
_logToStderr :: Bool,
logToStderr :: Bool,
-- | Whether to log to a file. Can be turned on together with
-- '_logToStderr'.
_logToFile :: Maybe FilePath,
-- 'logToStderr'.
logToFile :: Maybe FilePath,
-- | A formatting string for log timestamps. For the description of
-- available formatters, see 'formatTime'.
_logTimeFormat :: String
logTimeFormat :: String
}
deriving (Eq, Show)
$(pure [])
-- | Default instance: no base URL, no Google token, empty password, no
-- discussion link.
instance Default Config where
def = Config {
_baseUrl = "/",
_googleToken = "",
_adminPassword = "",
_discussLink = Nothing,
_matomoLink = Nothing,
_portMain = 8080,
_portApi = 4400,
_portEkg = 5050,
_cors = False,
_ekg = False,
_logToStderr = True,
_logToFile = Nothing,
_logTimeFormat = "%F %T UTC"
baseUrl = "/",
googleToken = "",
adminPassword = "",
discussLink = Nothing,
matomoLink = Nothing,
portMain = 8080,
portApi = 4400,
portEkg = 5050,
cors = False,
ekg = False,
logToStderr = True,
logToFile = Nothing,
logTimeFormat = "%F %T UTC"
}
instance FromJSON Config where
parseJSON = withObject "config" $ \o -> do
_baseUrl <- o .:? "base-url" .!= _baseUrl def
_googleToken <- o .:? "google-token" .!= _googleToken def
_adminPassword <- o .:? "admin-password" .!= _adminPassword def
_discussLink <- o .:? "discuss-link" .!= _discussLink def
_matomoLink <- o .:? "matomo-link" .!= _matomoLink def
_portMain <- o .:? "port-main" .!= _portMain def
_portApi <- o .:? "port-api" .!= _portApi def
_portEkg <- o .:? "port-ekg" .!= _portEkg def
_cors <- o .:? "cors" .!= _cors def
_ekg <- o .:? "ekg" .!= _ekg def
_logToStderr <- o .:? "log-to-stderr" .!= _logToStderr def
_logToFile <- o .:? "log-to-file" .!= _logToFile def
_logTimeFormat <- o .:? "log-time-format" .!= _logTimeFormat def
baseUrl <- o .:? "base-url" .!= baseUrl def
googleToken <- o .:? "google-token" .!= googleToken def
adminPassword <- o .:? "admin-password" .!= adminPassword def
discussLink <- o .:? "discuss-link" .!= discussLink def
matomoLink <- o .:? "matomo-link" .!= matomoLink def
portMain <- o .:? "port-main" .!= portMain def
portApi <- o .:? "port-api" .!= portApi def
portEkg <- o .:? "port-ekg" .!= portEkg def
cors <- o .:? "cors" .!= cors def
ekg <- o .:? "ekg" .!= ekg def
logToStderr <- o .:? "log-to-stderr" .!= logToStderr def
logToFile <- o .:? "log-to-file" .!= logToFile def
logTimeFormat <- o .:? "log-time-format" .!= logTimeFormat def
return Config{..}
instance ToJSON Config where
toJSON Config{..} = object [
"base-url" .= _baseUrl,
"google-token" .= _googleToken,
"admin-password" .= _adminPassword,
"discuss-link" .= _discussLink,
"matomo-link" .= _matomoLink,
"port-main" .= _portMain,
"port-api" .= _portApi,
"port-ekg" .= _portEkg,
"cors" .= _cors,
"ekg" .= _ekg,
"log-to-stderr" .= _logToStderr,
"log-to-file" .= _logToFile,
"log-time-format" .= _logTimeFormat
toJSON $(fields 'Config) = object [
"base-url" .= baseUrl,
"google-token" .= googleToken,
"admin-password" .= adminPassword,
"discuss-link" .= discussLink,
"matomo-link" .= matomoLink,
"port-main" .= portMain,
"port-api" .= portApi,
"port-ekg" .= portEkg,
"cors" .= cors,
"ekg" .= ekg,
"log-to-stderr" .= logToStderr,
"log-to-file" .= logToFile,
"log-time-format" .= logTimeFormat
]
-- | Read config from @config.json@ (creating a default config if the file

View File

@ -35,7 +35,7 @@ import Named
import Guide.Markdown (toMarkdownBlock, toMarkdownTree, toMarkdownInline)
import Guide.Types.Core (Category (..), CategoryStatus, Item (..), ItemSection, Trait (..),
TraitType)
import Guide.Utils (Uid (..), makeClassWithLenses)
import Guide.Utils (Uid (..), makeClassWithLenses, fields)
-- | Custom datatype errors for database
@ -108,6 +108,10 @@ makeClassWithLenses ''TraitRow
--
-- | To fetch items (they have an order) use 'getItemRowsByCategory' from 'Get' module.
-- | To fetch deleted items use 'getDeletedItemRowsByCategory' from 'Get' module
--
-- TODO: somehow handle the case when item IDs don't match the @itemsOrder@?
--
-- TODO: use 'fields' for pattern-matching.
categoryRowToCategory
:: "items" :! [Item]
-> "itemsDeleted" :! [Item]
@ -119,29 +123,32 @@ categoryRowToCategory
CategoryRow{..}
=
Category
{ _categoryUid = categoryRowUid
, _categoryTitle = categoryRowTitle
, _categoryCreated = categoryRowCreated
, _categoryGroup_ = categoryRowGroup
, _categoryStatus = categoryRowStatus
, _categoryNotes = toMarkdownBlock categoryRowNotes
, _categoryItems = items
, _categoryItemsDeleted = itemsDeleted
, _categoryEnabledSections = categoryRowEnabledSections
{ categoryUid = categoryRowUid
, categoryTitle = categoryRowTitle
, categoryCreated = categoryRowCreated
, categoryGroup = categoryRowGroup
, categoryStatus = categoryRowStatus
, categoryNotes = toMarkdownBlock categoryRowNotes
, categoryItems = items
, categoryItemsDeleted = itemsDeleted
, categoryEnabledSections = categoryRowEnabledSections
}
-- | Convert Category to CategoryRow.
categoryToRowCategory :: Category -> CategoryRow
categoryToRowCategory Category {..} = CategoryRow
{ categoryRowUid = _categoryUid
, categoryRowTitle = _categoryTitle
, categoryRowCreated = _categoryCreated
, categoryRowGroup = _categoryGroup_
, categoryRowStatus = _categoryStatus
, categoryRowNotes = toText $ show _categoryNotes -- TODO fix!
, categoryRowEnabledSections = _categoryEnabledSections
, categoryRowItemsOrder = map _itemUid _categoryItems
categoryToRowCategory $(fields 'Category) = CategoryRow
{ categoryRowUid = categoryUid
, categoryRowTitle = categoryTitle
, categoryRowCreated = categoryCreated
, categoryRowGroup = categoryGroup
, categoryRowStatus = categoryStatus
, categoryRowNotes = toText $ show categoryNotes -- TODO fix!
, categoryRowEnabledSections = categoryEnabledSections
, categoryRowItemsOrder = map itemUid categoryItems
}
where
-- Ignored fields
_ = categoryItemsDeleted
-- | Convert ItemRow to Item.
--
@ -162,44 +169,47 @@ itemRowToItem
ItemRow{..}
=
Item
{ _itemUid = itemRowUid
, _itemName = itemRowName
, _itemCreated = itemRowCreated
, _itemHackage = itemRowHackage
, _itemSummary = toMarkdownBlock itemRowSummary
, _itemPros = proTraits
, _itemProsDeleted = proDeletedTraits
, _itemCons = conTraits
, _itemConsDeleted = conDeletedTraits
, _itemEcosystem = toMarkdownBlock itemRowEcosystem
, _itemNotes = toMarkdownTree prefix itemRowNotes
, _itemLink = itemRowLink
{ itemUid = itemRowUid
, itemName = itemRowName
, itemCreated = itemRowCreated
, itemHackage = itemRowHackage
, itemSummary = toMarkdownBlock itemRowSummary
, itemPros = proTraits
, itemProsDeleted = proDeletedTraits
, itemCons = conTraits
, itemConsDeleted = conDeletedTraits
, itemEcosystem = toMarkdownBlock itemRowEcosystem
, itemNotes = toMarkdownTree prefix itemRowNotes
, itemLink = itemRowLink
}
where
prefix = "item-notes-" <> uidToText itemRowUid <> "-"
-- | Convert Item to ItemRow.
itemToRowItem :: Uid Category -> "deleted" :! Bool -> Item -> ItemRow
itemToRowItem catId (arg #deleted -> deleted) Item{..} = ItemRow
{ itemRowUid = _itemUid
, itemRowName = _itemName
, itemRowCreated = _itemCreated
, itemRowLink = _itemLink
, itemRowHackage = _itemHackage
, itemRowSummary = toText $ show _itemSummary
, itemRowEcosystem = toText $ show _itemEcosystem
, itemRowNotes = toText $ show _itemNotes
itemToRowItem catId (arg #deleted -> deleted) $(fields 'Item) = ItemRow
{ itemRowUid = itemUid
, itemRowName = itemName
, itemRowCreated = itemCreated
, itemRowLink = itemLink
, itemRowHackage = itemHackage
, itemRowSummary = toText $ show itemSummary -- TODO fix
, itemRowEcosystem = toText $ show itemEcosystem -- TODO fix
, itemRowNotes = toText $ show itemNotes -- TODO fix
, itemRowDeleted = deleted
, itemRowCategoryUid = catId
, itemRowProsOrder = map _traitUid _itemPros
, itemRowConsOrder = map _traitUid _itemCons
, itemRowProsOrder = map traitUid itemPros
, itemRowConsOrder = map traitUid itemCons
}
where
-- Ignored fields
_ = (itemConsDeleted, itemProsDeleted)
-- | Convert TraitRow to Trait.
traitRowToTrait :: TraitRow -> Trait
traitRowToTrait TraitRow{..} = Trait
{ _traitUid = traitRowUid
, _traitContent = toMarkdownInline traitRowContent
{ traitUid = traitRowUid
, traitContent = toMarkdownInline traitRowContent
}
-- Convert Trait to TraitRow
@ -209,10 +219,10 @@ traitToTraitRow
-> TraitType
-> Trait
-> TraitRow
traitToTraitRow itemId (arg #deleted -> deleted) traitType Trait{..} =
traitToTraitRow itemId (arg #deleted -> deleted) traitType $(fields 'Trait) =
TraitRow
{ traitRowUid = _traitUid
, traitRowContent = toText $ show _traitContent
{ traitRowUid = traitUid
, traitRowContent = toText $ show traitContent -- TODO fix
, traitRowDeleted = deleted
, traitRowType = traitType
, traitRowItemUid = itemId

View File

@ -25,6 +25,7 @@ import Data.Vector (Vector)
import Guide.Diff.Merge (merge)
import Guide.Diff.Tokenize (tokenize)
import Guide.Utils (makeClassWithLenses)
import qualified Data.Patch as PV
import qualified Data.Vector as V
@ -45,9 +46,7 @@ data DiffChunk
| Plain Text -- ^ This part should be rendered as not modified
deriving (Eq, Show)
flip makeLensesFor ''Diff
[ ("diffContextAbove", "_diffContextAbove")
, ("diffContextBelow", "_diffContextBelow") ]
makeClassWithLenses ''Diff
diff
:: Text -- ^ Original text

View File

@ -101,7 +101,7 @@ setMethods = do
addEdit edit
do (edit, _) <- dbUpdate (SetCategoryStatus catId status')
addEdit edit
do oldEnabledSections <- view enabledSections <$> dbQuery (GetCategory catId)
do oldEnabledSections <- categoryEnabledSections <$> dbQuery (GetCategory catId)
let newEnabledSections = S.fromList . concat $
[ [ItemProsConsSection | prosConsEnabled']
, [ItemEcosystemSection | ecosystemEnabled']
@ -118,7 +118,7 @@ setMethods = do
Spock.post (setRoute <//> categoryVar <//> "notes") $ \catId -> do
original <- param' "original"
content' <- param' "content"
modified <- view (notes.mdSource) <$> dbQuery (GetCategory catId)
modified <- markdownBlockSource . categoryNotes <$> dbQuery (GetCategory catId)
if modified == original
then do
(edit, category) <- dbUpdate (SetCategoryNotes catId content')
@ -161,7 +161,7 @@ setMethods = do
Spock.post (setRoute <//> itemVar <//> "description") $ \itemId -> do
original <- param' "original"
content' <- param' "content"
modified <- view (summary.mdSource) <$> dbQuery (GetItem itemId)
modified <- markdownBlockSource . itemSummary <$> dbQuery (GetItem itemId)
if modified == original
then do
(edit, item) <- dbUpdate (SetItemSummary itemId content')
@ -176,7 +176,7 @@ setMethods = do
Spock.post (setRoute <//> itemVar <//> "ecosystem") $ \itemId -> do
original <- param' "original"
content' <- param' "content"
modified <- view (ecosystem.mdSource) <$> dbQuery (GetItem itemId)
modified <- markdownBlockSource . itemEcosystem <$> dbQuery (GetItem itemId)
if modified == original
then do
(edit, item) <- dbUpdate (SetItemEcosystem itemId content')
@ -191,7 +191,7 @@ setMethods = do
Spock.post (setRoute <//> itemVar <//> "notes") $ \itemId -> do
original <- param' "original"
content' <- param' "content"
modified <- view (notes.mdSource) <$> dbQuery (GetItem itemId)
modified <- markdownTreeSource . itemNotes <$> dbQuery (GetItem itemId)
if modified == original
then do
(edit, item) <- dbUpdate (SetItemNotes itemId content')
@ -207,7 +207,8 @@ setMethods = do
Spock.post (setRoute <//> itemVar <//> traitVar) $ \itemId traitId -> do
original <- param' "original"
content' <- param' "content"
modified <- view (content.mdSource) <$> dbQuery (GetTrait itemId traitId)
modified <- markdownInlineSource . traitContent <$>
dbQuery (GetTrait itemId traitId)
if modified == original
then do
(edit, trait) <- dbUpdate (SetTraitContent itemId traitId content')
@ -286,18 +287,18 @@ otherMethods = do
-- # Feeds
-- TODO: this link shouldn't be absolute [absolute-links]
baseUrl <- (// "haskell") . _baseUrl <$> getConfig
baseUrl <- (// "haskell") . baseUrl <$> getConfig
-- Feed for items in a category
Spock.get (feedRoute <//> categoryVar) $ \catId -> do
category <- dbQuery (GetCategory catId)
let sortedItems = sortBy (flip cmp) (category^.items)
where cmp = comparing (^.created) <> comparing (^.uid)
let sortedItems = sortBy (flip cmp) (categoryItems category)
where cmp = comparing itemCreated <> comparing itemUid
let route = "feed" <//> categoryVar
let feedUrl = baseUrl // Spock.renderRoute route (category^.uid)
feedTitle = Atom.TextString (category^.title <> " Haskell Aelve Guide")
let feedUrl = baseUrl // Spock.renderRoute route (categoryUid category)
feedTitle = Atom.TextString (categoryTitle category <> " Haskell Aelve Guide")
feedLastUpdate = case sortedItems of
item:_ -> Feed.toFeedDateStringUTC Feed.AtomKind (item^.created)
item:_ -> Feed.toFeedDateStringUTC Feed.AtomKind (itemCreated item)
_ -> ""
let feedBase = Atom.nullFeed feedUrl feedTitle (toText feedLastUpdate)
entries <- liftIO $ mapM (itemToFeedEntry baseUrl category) sortedItems
@ -364,8 +365,8 @@ itemToFeedEntry baseUrl category item = do
Atom.entryContent = Just (Atom.HTMLContent (toText entryContent)) }
where
entryLink = baseUrl //
format "{}#item-{}" (categorySlug category) (item^.uid)
format "{}#item-{}" (categorySlug category) (itemUid item)
entryBase = Atom.nullEntry
(uidToText (item^.uid))
(Atom.TextString (item^.name))
(toText (Feed.toFeedDateStringUTC Feed.AtomKind (item^.created)))
(uidToText (itemUid item))
(Atom.TextString (itemName item))
(toText (Feed.toFeedDateStringUTC Feed.AtomKind (itemCreated item)))

View File

@ -41,11 +41,11 @@ withLogger :: Config -> (Logger -> IO ()) -> IO ()
withLogger Config{..} act = do
logLvlEnv <- lookupEnv "LOG_LEVEL"
let logLvl = fromMaybe Debug (readMaybe =<< logLvlEnv)
mbWithFile _logToFile AppendMode $ \logFileHandle -> do
mbWithFile logToFile AppendMode $ \logFileHandle -> do
let logHandler logLine@(Di.Core.Log _ lvl _ _) =
when (lvl >= logLvl) $ do
let formattedLogLine = showLogLine _logTimeFormat logLine
when _logToStderr $ sayErr formattedLogLine
let formattedLogLine = showLogLine logTimeFormat logLine
when logToStderr $ sayErr formattedLogLine
whenJust logFileHandle $ \h -> hSay h formattedLogLine
Di.Core.new logHandler $ \logger ->
act logger `catch` \(e :: SomeException) ->

View File

@ -160,7 +160,7 @@ mainWith config@Config{..} = withLogger config $ \logger -> do
ekgId <- newIORef Nothing
workFinished <- newEmptyMVar
let finishWork = do
when _ekg $ do
when ekg $ do
-- Killing EKG has to be done last, because of
-- <https://github.com/tibbe/ekg/issues/62>
logDebugIO logger "Killing EKG"
@ -200,19 +200,19 @@ ekgMetrics
-> IORef (Maybe ThreadId)
-> IO (Maybe EKG.WaiMetrics)
ekgMetrics logger Config{..} db ekgId =
if _ekg
if ekg
then do
ekg <- do
logDebugIO logger $ format "EKG is running on port {}" _portEkg
EKG.forkServer "localhost" _portEkg
writeIORef ekgId (Just (EKG.serverThreadId ekg))
waiMetrics <- EKG.registerWaiMetrics (EKG.serverMetricStore ekg)
categoryGauge <- EKG.getGauge "db.categories" ekg
itemGauge <- EKG.getGauge "db.items" ekg
ekgServer <- do
logDebugIO logger $ format "EKG is running on port {}" portEkg
EKG.forkServer "localhost" portEkg
writeIORef ekgId (Just (EKG.serverThreadId ekgServer))
waiMetrics <- EKG.registerWaiMetrics (EKG.serverMetricStore ekgServer)
categoryGauge <- EKG.getGauge "db.categories" ekgServer
itemGauge <- EKG.getGauge "db.items" ekgServer
void $ async $ forever $ do
globalState <- Acid.query db GetGlobalState
let allCategories = globalState^.categories
let allItems = allCategories^..each.items.each
let allCategories = categories globalState
let allItems = allCategories ^.. each . _categoryItems . each
EKG.Gauge.set categoryGauge (fromIntegral (length allCategories))
EKG.Gauge.set itemGauge (fromIntegral (length allItems))
threadDelay (1000000 * 60)
@ -242,8 +242,8 @@ runOldServer logger config@Config{..} db mWaiMetrics = do
spc_maxRequestSize = Just (1024*1024),
spc_csrfProtection = True,
spc_sessionCfg = sessionCfg }
logDebugIO logger $ format "Spock is running on port {}" _portMain
runSpockNoBanner _portMain $ spock spockConfig $ guideApp mWaiMetrics
logDebugIO logger $ format "Spock is running on port {}" portMain
runSpockNoBanner portMain $ spock spockConfig $ guideApp mWaiMetrics
-- TODO: Fix indentation after rebasing.
guideApp :: Maybe EKG.WaiMetrics -> GuideApp ()
@ -350,7 +350,7 @@ loginAction = do
LoginUser loginEmail (toByteString loginUserPassword)
case loginAttempt of
Right user -> do
modifySession (sessionUserID ?~ (user ^. userID))
modifySession (sessionUserID ?~ userID user)
Spock.redirect "/"
-- TODO: *properly* show error message/validation of input
Left err -> do
@ -377,7 +377,7 @@ signupAction = do
success <- dbUpdate $ CreateUser user
if success
then do
modifySession (sessionUserID ?~ (user ^. userID))
modifySession (sessionUserID ?~ userID user)
Spock.redirect ""
else do
formHtml <- protectForm registerFormView v
@ -398,7 +398,7 @@ adminHook :: ListContains n User xs => GuideAction (HVect xs) (HVect (IsAdmin ':
adminHook = do
oldCtx <- getContext
let user = findFirst oldCtx
if user ^. userIsAdmin
if userIsAdmin user
then return (IsAdmin :&: oldCtx)
else Spock.text "Not authorized."
@ -435,6 +435,6 @@ installTerminationCatcher thread = void $ do
createAdminUser :: GuideApp ()
createAdminUser = do
dbUpdate DeleteAllUsers
pass <- toByteString . _adminPassword <$> getConfig
pass <- toByteString . adminPassword <$> getConfig
user <- makeUser "admin" "admin@guide.aelve.com" pass
void $ dbUpdate $ CreateUser (user & userIsAdmin .~ True)
void $ dbUpdate $ CreateUser (user & _userIsAdmin .~ True)

View File

@ -13,18 +13,13 @@ module Guide.Markdown
(
-- * Types
MarkdownInline(..),
MarkdownInlineLenses(..),
MarkdownBlock(..),
MarkdownBlockLenses(..),
MarkdownTree(..),
MarkdownTreeLenses(..),
Heading(..),
-- * Lenses
mdHtml,
mdSource,
mdMarkdown,
mdIdPrefix,
mdTree,
mdTOC,
-- * Converting text to Markdown
toMarkdownInline,
toMarkdownBlock,
@ -32,7 +27,6 @@ module Guide.Markdown
-- * Misc
renderMD,
markdownNull,
extractPreface,
)
where
@ -66,33 +60,33 @@ import qualified Data.Text as T
data MarkdownInline = MarkdownInline {
markdownInlineMdSource :: Text,
markdownInlineMdHtml :: ByteString,
markdownInlineMdMarkdown :: ![MD.Node] }
markdownInlineSource :: Text,
markdownInlineHtml :: ByteString,
markdownInlineMarkdown :: ![MD.Node] }
deriving (Generic, Data, Eq)
data MarkdownBlock = MarkdownBlock {
markdownBlockMdSource :: Text,
markdownBlockMdHtml :: ByteString,
markdownBlockMdMarkdown :: ![MD.Node] }
markdownBlockSource :: Text,
markdownBlockHtml :: ByteString,
markdownBlockMarkdown :: ![MD.Node] }
deriving (Generic, Data)
data MarkdownTree = MarkdownTree {
markdownTreeMdSource :: Text,
markdownTreeMdTree :: !(Document Text ByteString),
markdownTreeMdIdPrefix :: Text,
markdownTreeMdTOC :: Forest Heading }
markdownTreeSource :: Text,
markdownTreeStructure :: !(Document Text ByteString),
markdownTreeIdPrefix :: Text,
markdownTreeTOC :: Forest Heading }
deriving (Generic, Data)
-- | Table-of-contents heading
data Heading = Heading
{ headingMd :: MarkdownInline
{ headingMarkdown :: MarkdownInline
, headingSlug :: Text
} deriving (Generic, Data, Eq)
makeFields ''MarkdownInline
makeFields ''MarkdownBlock
makeFields ''MarkdownTree
makeClassWithLenses ''MarkdownInline
makeClassWithLenses ''MarkdownBlock
makeClassWithLenses ''MarkdownTree
parseMD :: Text -> [MD.Node]
parseMD s =
@ -149,16 +143,17 @@ stringify = T.concat . map go
-- | Extract everything before the first heading.
--
-- Note that if you render 'mdSource' of the produced Markdown block, it won't
-- necessarily parse into 'mdHtml' from the same block. It's because rendered
-- Markdown might depend on links that are defined further in the tree.
-- Note that if you render 'markdownBlockSource' of the produced Markdown
-- block, it won't necessarily parse into 'markdownBlockHtml' from the same
-- block. It's because rendered Markdown might depend on links that are
-- defined further in the tree.
extractPreface :: MarkdownTree -> MarkdownBlock
extractPreface = mkBlock . preface . view mdTree
extractPreface = mkBlock . preface . markdownTreeStructure
where
mkBlock x = MarkdownBlock {
markdownBlockMdSource = getSource x,
markdownBlockMdHtml = renderMD (stripSource x),
markdownBlockMdMarkdown = stripSource x }
markdownBlockSource = getSource x,
markdownBlockHtml = renderMD (stripSource x),
markdownBlockMarkdown = stripSource x }
-- | Flatten Markdown by concatenating all block elements.
extractInlines :: [MD.Node] -> [MD.Node]
@ -241,28 +236,28 @@ parseLink = either (Left . show) Right . parse p ""
toMarkdownInline :: Text -> MarkdownInline
toMarkdownInline s = MarkdownInline {
markdownInlineMdSource = s,
markdownInlineMdHtml = html,
markdownInlineMdMarkdown = inlines }
markdownInlineSource = s,
markdownInlineHtml = html,
markdownInlineMarkdown = inlines }
where
inlines = extractInlines (parseMD s)
html = renderMD inlines
toMarkdownBlock :: Text -> MarkdownBlock
toMarkdownBlock s = MarkdownBlock {
markdownBlockMdSource = s,
markdownBlockMdHtml = html,
markdownBlockMdMarkdown = doc }
markdownBlockSource = s,
markdownBlockHtml = html,
markdownBlockMarkdown = doc }
where
doc = parseMD s
html = renderMD doc
toMarkdownTree :: Text -> Text -> MarkdownTree
toMarkdownTree idPrefix s = MarkdownTree {
markdownTreeMdSource = s,
markdownTreeMdIdPrefix = idPrefix,
markdownTreeMdTree = tree,
markdownTreeMdTOC = toc }
markdownTreeSource = s,
markdownTreeIdPrefix = idPrefix,
markdownTreeStructure = tree,
markdownTreeTOC = toc }
where
blocks :: [MD.Node]
blocks = parseMD s
@ -281,9 +276,9 @@ toMarkdownTree idPrefix s = MarkdownTree {
nodesToMdInline :: WithSource [MD.Node] -> MarkdownInline
nodesToMdInline (WithSource src nodes) = MarkdownInline
{ markdownInlineMdSource = src
, markdownInlineMdHtml = html
, markdownInlineMdMarkdown = inlines
{ markdownInlineSource = src
, markdownInlineHtml = html
, markdownInlineMarkdown = inlines
}
where
inlines = extractInlines nodes
@ -310,34 +305,34 @@ slugifyDocument slugify doc = doc {
return sec{headingAnn = slug}
instance Show MarkdownInline where
show = show . view mdSource
show = show . markdownInlineSource
instance Show MarkdownBlock where
show = show . view mdSource
show = show . markdownBlockSource
instance Show MarkdownTree where
show = show . view mdSource
show = show . markdownTreeSource
deriving instance Show Heading
instance A.ToJSON MarkdownInline where
toJSON md = A.object [
"text" A..= (md^.mdSource),
"html" A..= toText (md^.mdHtml) ]
"text" A..= markdownInlineSource md,
"html" A..= toText (markdownInlineHtml md) ]
instance A.ToJSON MarkdownBlock where
toJSON md = A.object [
"text" A..= (md^.mdSource),
"html" A..= toText (md^.mdHtml) ]
"text" A..= markdownBlockSource md,
"html" A..= toText (markdownBlockHtml md) ]
instance A.ToJSON MarkdownTree where
toJSON md = A.object [
"text" A..= (md^.mdSource) ]
"text" A..= markdownTreeSource md ]
instance ToHtml MarkdownInline where
toHtmlRaw = toHtml
toHtml = toHtmlRaw . view mdHtml
toHtml = toHtmlRaw . markdownInlineHtml
instance ToHtml MarkdownBlock where
toHtmlRaw = toHtml
toHtml = toHtmlRaw . view mdHtml
toHtml = toHtmlRaw . markdownBlockHtml
instance ToHtml MarkdownTree where
toHtmlRaw = toHtml
toHtml = toHtmlRaw . renderDoc . view mdTree
toHtml = toHtmlRaw . renderDoc . markdownTreeStructure
where
renderDoc Document{..} = BS.concat $
prefaceAnn :
@ -356,22 +351,18 @@ instance ToHtml MarkdownTree where
instance SafeCopy MarkdownInline where
version = 0
kind = base
putCopy = contain . safePut . view mdSource
putCopy = contain . safePut . markdownInlineSource
getCopy = contain $ toMarkdownInline <$> safeGet
instance SafeCopy MarkdownBlock where
version = 0
kind = base
putCopy = contain . safePut . view mdSource
putCopy = contain . safePut . markdownBlockSource
getCopy = contain $ toMarkdownBlock <$> safeGet
instance SafeCopy MarkdownTree where
version = 0
kind = base
putCopy md = contain $ do
safePut (md ^. mdIdPrefix)
safePut (md ^. mdSource)
safePut (markdownTreeIdPrefix md)
safePut (markdownTreeSource md)
getCopy = contain $
toMarkdownTree <$> safeGet <*> safeGet
-- | Is a piece of Markdown empty?
markdownNull :: HasMdSource a Text => a -> Bool
markdownNull = T.null . view mdSource

View File

@ -41,7 +41,7 @@ data Matomo = Matomo
postMatomo :: Matomo -> Guider ()
postMatomo Matomo{..} = push "postMatomo" $ do
Context Config{..} _ _ <- ask
whenJust _matomoLink $ \matomo -> liftIO $
whenJust matomoLink $ \matomo -> liftIO $
void $ async $ do
manager <- getGlobalManager
req <- setQueryString

View File

@ -37,20 +37,20 @@ search :: Text -> GlobalState -> [SearchResult]
search query gs =
-- category titles
sortByRank [(SRCategory cat, rank)
| cat <- gs^.categories
, let rank = match query (cat^.title)
| cat <- categories gs
, let rank = match query (categoryTitle cat)
, rank > 0 ] ++
-- item names
sortByRank [(SRItem cat item, rank)
| cat <- gs^.categories
, item <- cat^.items
, let rank = match query (item^.name)
| cat <- categories gs
, item <- categoryItems cat
, let rank = match query (itemName item)
, rank > 0 ] ++
-- item ecosystems
sortByRank [(SRItemEcosystem cat item, rank)
| cat <- gs^.categories
, item <- cat^.items
, let rank = match query (item^.ecosystem.mdSource)
| cat <- categories gs
, item <- categoryItems cat
, let rank = match query (markdownBlockSource (itemEcosystem item))
, rank > 0 ]
where
sortByRank :: [(a, Int)] -> [a]

View File

@ -108,64 +108,64 @@ undoEdit (Edit'AddPro itemId traitId _) = do
undoEdit (Edit'AddCon itemId traitId _) = do
void <$> dbUpdate (DeleteTrait itemId traitId)
undoEdit (Edit'SetCategoryTitle catId old new) = do
now <- view title <$> dbQuery (GetCategory catId)
now <- categoryTitle <$> dbQuery (GetCategory catId)
if now /= new
then return (Left "title has been changed further")
else Right () <$ dbUpdate (SetCategoryTitle catId old)
undoEdit (Edit'SetCategoryGroup catId old new) = do
now <- view group_ <$> dbQuery (GetCategory catId)
now <- categoryGroup <$> dbQuery (GetCategory catId)
if now /= new
then return (Left "group has been changed further")
else Right () <$ dbUpdate (SetCategoryGroup catId old)
undoEdit (Edit'SetCategoryStatus catId old new) = do
now <- view status <$> dbQuery (GetCategory catId)
now <- categoryStatus <$> dbQuery (GetCategory catId)
if now /= new
then return (Left "status has been changed further")
else Right () <$ dbUpdate (SetCategoryStatus catId old)
undoEdit (Edit'ChangeCategoryEnabledSections catId toEnable toDisable) = do
enabledNow <- view enabledSections <$> dbQuery (GetCategory catId)
enabledNow <- categoryEnabledSections <$> dbQuery (GetCategory catId)
if any (`elem` enabledNow) toDisable || any (`notElem` enabledNow) toEnable
then return (Left "enabled-sections has been changed further")
else Right () <$ dbUpdate (ChangeCategoryEnabledSections catId toDisable toEnable)
undoEdit (Edit'SetCategoryNotes catId old new) = do
now <- view (notes.mdSource) <$> dbQuery (GetCategory catId)
now <- markdownBlockSource . categoryNotes <$> dbQuery (GetCategory catId)
if now /= new
then return (Left "notes have been changed further")
else Right () <$ dbUpdate (SetCategoryNotes catId old)
undoEdit (Edit'SetItemName itemId old new) = do
now <- view name <$> dbQuery (GetItem itemId)
now <- itemName <$> dbQuery (GetItem itemId)
if now /= new
then return (Left "name has been changed further")
else Right () <$ dbUpdate (SetItemName itemId old)
undoEdit (Edit'SetItemLink itemId old new) = do
now <- view link <$> dbQuery (GetItem itemId)
now <- itemLink <$> dbQuery (GetItem itemId)
if now /= new
then return (Left "link has been changed further")
else Right () <$ dbUpdate (SetItemLink itemId old)
undoEdit (Edit'SetItemGroup _ _ _) = do
return (Left "groups are not supported anymore")
undoEdit (Edit'SetItemHackage itemId old new) = do
now <- view hackage <$> dbQuery (GetItem itemId)
now <- itemHackage <$> dbQuery (GetItem itemId)
if now /= new
then return (Left "Hackage name has been changed further")
else Right () <$ dbUpdate (SetItemHackage itemId old)
undoEdit (Edit'SetItemSummary itemId old new) = do
now <- view (summary.mdSource) <$> dbQuery (GetItem itemId)
now <- markdownBlockSource . itemSummary <$> dbQuery (GetItem itemId)
if now /= new
then return (Left "description has been changed further")
else Right () <$ dbUpdate (SetItemSummary itemId old)
undoEdit (Edit'SetItemNotes itemId old new) = do
now <- view (notes.mdSource) <$> dbQuery (GetItem itemId)
now <- markdownTreeSource . itemNotes <$> dbQuery (GetItem itemId)
if now /= new
then return (Left "notes have been changed further")
else Right () <$ dbUpdate (SetItemNotes itemId old)
undoEdit (Edit'SetItemEcosystem itemId old new) = do
now <- view (ecosystem.mdSource) <$> dbQuery (GetItem itemId)
now <- markdownBlockSource . itemEcosystem <$> dbQuery (GetItem itemId)
if now /= new
then return (Left "ecosystem has been changed further")
else Right () <$ dbUpdate (SetItemEcosystem itemId old)
undoEdit (Edit'SetTraitContent itemId traitId old new) = do
now <- view (content.mdSource) <$> dbQuery (GetTrait itemId traitId)
now <- markdownInlineSource . traitContent <$> dbQuery (GetTrait itemId traitId)
if now /= new
then return (Left "trait has been changed further")
else Right () <$ dbUpdate (SetTraitContent itemId traitId old)

View File

@ -19,11 +19,8 @@ module Guide.State
-- * type of global state
GlobalState(..),
categories,
categoriesDeleted,
pendingEdits,
editIdCounter,
findCategoryByItem,
GlobalStateLenses(..),
findCategoryByItem,
emptyState,
-- * acid-state methods
@ -180,42 +177,42 @@ Guide.hs
emptyState :: GlobalState
emptyState = GlobalState {
_categories = [],
_categoriesDeleted = [],
_pendingEdits = [],
_editIdCounter = 0,
_sessionStore = M.empty,
_users = M.empty,
_dirty = True }
categories = [],
categoriesDeleted = [],
pendingEdits = [],
editIdCounter = 0,
sessionStore = M.empty,
users = M.empty,
dirty = True }
data GlobalState = GlobalState {
_categories :: [Category],
_categoriesDeleted :: [Category],
categories :: [Category],
categoriesDeleted :: [Category],
-- | Pending edits, newest first
_pendingEdits :: [(Edit, EditDetails)],
pendingEdits :: [(Edit, EditDetails)],
-- | ID of next edit that will be made
_editIdCounter :: Int,
editIdCounter :: Int,
-- | Sessions
_sessionStore :: Map SessionId GuideSession,
sessionStore :: Map SessionId GuideSession,
-- | Users
_users :: Map (Uid User) User,
users :: Map (Uid User) User,
-- | The dirty bit (needed to choose whether to make a checkpoint or not)
_dirty :: Bool }
dirty :: Bool }
deriving (Show)
deriveSafeCopySorted 9 'extension ''GlobalState
makeLenses ''GlobalState
makeClassWithLenses ''GlobalState
changelog ''GlobalState (Current 9, Past 8) [
-- TODO: it's silly that we have to reference 'Action' and keep it in the
-- codebase even though we have no use for 'Action' anymore
Removed "_actions" [t|[(Action, ActionDetails)]|]
Removed "actions" [t|[(Action, ActionDetails)]|]
]
deriveSafeCopySorted 8 'extension ''GlobalState_v8
changelog ''GlobalState (Past 8, Past 7) [
Added "_sessionStore" [hs|M.empty|],
Added "_users" [hs|M.empty|]
Added "sessionStore" [hs|M.empty|],
Added "users" [hs|M.empty|]
]
deriveSafeCopySorted 7 'base ''GlobalState_v7
@ -227,8 +224,8 @@ traitById traitId = singular $
maybeTraitById :: Uid Trait -> Traversal' Item Trait
maybeTraitById traitId =
(pros.each . filtered (hasUid traitId)) `failing`
(cons.each . filtered (hasUid traitId))
(_itemPros . each . filtered ((== traitId) . traitUid)) `failing`
(_itemCons . each . filtered ((== traitId) . traitUid))
categoryById :: Uid Category -> Lens' GlobalState Category
categoryById catId = singular $
@ -237,7 +234,7 @@ categoryById catId = singular $
toString (uidToText catId))
maybeCategoryById :: Uid Category -> Traversal' GlobalState Category
maybeCategoryById catId = categories.each . filtered (hasUid catId)
maybeCategoryById catId = _categories . each . filtered ((== catId) . categoryUid)
itemById :: Uid Item -> Lens' GlobalState Item
itemById itemId = singular $
@ -246,15 +243,16 @@ itemById itemId = singular $
toString (uidToText itemId))
maybeItemById :: Uid Item -> Traversal' GlobalState Item
maybeItemById itemId = categories.each . items.each . filtered (hasUid itemId)
maybeItemById itemId =
_categories . each . _categoryItems . each . filtered ((== itemId) . itemUid)
findCategoryByItem :: Uid Item -> GlobalState -> Category
findCategoryByItem itemId s =
fromMaybe (error err) (find hasItem (s^.categories))
fromMaybe (error err) (find hasItem (categories s))
where
err = "findCategoryByItem: couldn't find category with item with uid " ++
toString (uidToText itemId)
hasItem category = itemId `elem` (category^..items.each.uid)
hasItem category = itemId `elem` (category ^.. _categoryItems . each . _itemUid)
-- | 'PublicDB' contains all safe data from 'GlobalState'.
-- Difference from 'GlobalState':
@ -277,27 +275,30 @@ deriveSafeCopySorted 1 'base ''PublicDB
-- | Converts 'GlobalState' to 'PublicDB' type stripping private data.
toPublicDB :: GlobalState -> PublicDB
toPublicDB GlobalState{..} =
toPublicDB $(fields 'GlobalState) =
PublicDB {
publicCategories = _categories,
publicCategoriesDeleted = _categoriesDeleted,
publicPendingEdits = _pendingEdits,
publicEditIdCounter = _editIdCounter,
publicUsers = fmap userToPublic _users
publicCategories = categories,
publicCategoriesDeleted = categoriesDeleted,
publicPendingEdits = pendingEdits,
publicEditIdCounter = editIdCounter,
publicUsers = fmap userToPublic users
}
where
-- Ignored fields
_ = (dirty, sessionStore)
-- | Converts 'PublicDB' to 'GlobalState' type filling in non-existing data with
-- default values.
fromPublicDB :: PublicDB -> GlobalState
fromPublicDB PublicDB{..} =
fromPublicDB $(fields 'PublicDB) =
GlobalState {
_categories = publicCategories,
_categoriesDeleted = publicCategoriesDeleted,
_pendingEdits = publicPendingEdits,
_editIdCounter = publicEditIdCounter,
_sessionStore = M.empty,
_users = fmap publicUserToUser publicUsers,
_dirty = True
categories = publicCategories,
categoriesDeleted = publicCategoriesDeleted,
pendingEdits = publicPendingEdits,
editIdCounter = publicEditIdCounter,
sessionStore = M.empty,
users = fmap publicUserToUser publicUsers,
dirty = True
}
-- get
@ -306,7 +307,7 @@ getGlobalState :: Acid.Query GlobalState GlobalState
getGlobalState = view id
getCategories :: Acid.Query GlobalState [Category]
getCategories = view categories
getCategories = view _categories
getCategory :: Uid Category -> Acid.Query GlobalState Category
getCategory uid' = view (categoryById uid')
@ -342,19 +343,19 @@ addCategory
-> Acid.Update GlobalState (Edit, Category)
addCategory catId title' group' created' = do
let newCategory = Category {
_categoryUid = catId,
_categoryTitle = title',
_categoryGroup_ = group',
_categoryEnabledSections = S.fromList [
categoryUid = catId,
categoryTitle = title',
categoryGroup = group',
categoryEnabledSections = S.fromList [
ItemProsConsSection,
ItemEcosystemSection,
ItemNotesSection ],
_categoryCreated = created',
_categoryStatus = CategoryStub,
_categoryNotes = toMarkdownBlock "",
_categoryItems = [],
_categoryItemsDeleted = [] }
categories %= (newCategory :)
categoryCreated = created',
categoryStatus = CategoryStub,
categoryNotes = toMarkdownBlock "",
categoryItems = [],
categoryItemsDeleted = [] }
_categories %= (newCategory :)
let edit = Edit'AddCategory catId title' group'
return (edit, newCategory)
@ -366,20 +367,20 @@ addItem
-> Acid.Update GlobalState (Edit, Item)
addItem catId itemId name' created' = do
let newItem = Item {
_itemUid = itemId,
_itemName = name',
_itemCreated = created',
_itemHackage = Nothing,
_itemSummary = toMarkdownBlock "",
_itemPros = [],
_itemProsDeleted = [],
_itemCons = [],
_itemConsDeleted = [],
_itemEcosystem = toMarkdownBlock "",
_itemNotes = let pref = "item-notes-" <> uidToText itemId <> "-"
in toMarkdownTree pref "",
_itemLink = Nothing}
categoryById catId . items %= (++ [newItem])
itemUid = itemId,
itemName = name',
itemCreated = created',
itemHackage = Nothing,
itemSummary = toMarkdownBlock "",
itemPros = [],
itemProsDeleted = [],
itemCons = [],
itemConsDeleted = [],
itemEcosystem = toMarkdownBlock "",
itemNotes = let pref = "item-notes-" <> uidToText itemId <> "-"
in toMarkdownTree pref "",
itemLink = Nothing}
categoryById catId . _categoryItems %= (++ [newItem])
let edit = Edit'AddItem catId itemId name'
return (edit, newItem)
@ -390,7 +391,7 @@ addPro
-> Acid.Update GlobalState (Edit, Trait)
addPro itemId traitId text' = do
let newTrait = Trait traitId (toMarkdownInline text')
itemById itemId . pros %= (++ [newTrait])
itemById itemId . _itemPros %= (++ [newTrait])
let edit = Edit'AddPro itemId traitId text'
return (edit, newTrait)
@ -401,7 +402,7 @@ addCon
-> Acid.Update GlobalState (Edit, Trait)
addCon itemId traitId text' = do
let newTrait = Trait traitId (toMarkdownInline text')
itemById itemId . cons %= (++ [newTrait])
itemById itemId . _itemCons %= (++ [newTrait])
let edit = Edit'AddCon itemId traitId text'
return (edit, newTrait)
@ -417,25 +418,25 @@ setGlobalState = (id .=)
setCategoryTitle :: Uid Category -> Text -> Acid.Update GlobalState (Edit, Category)
setCategoryTitle catId title' = do
oldTitle <- categoryById catId . title <<.= title'
oldTitle <- categoryById catId . _categoryTitle <<.= title'
let edit = Edit'SetCategoryTitle catId oldTitle title'
(edit,) <$> use (categoryById catId)
setCategoryGroup :: Uid Category -> Text -> Acid.Update GlobalState (Edit, Category)
setCategoryGroup catId group' = do
oldGroup <- categoryById catId . group_ <<.= group'
oldGroup <- categoryById catId . _categoryGroup <<.= group'
let edit = Edit'SetCategoryGroup catId oldGroup group'
(edit,) <$> use (categoryById catId)
setCategoryNotes :: Uid Category -> Text -> Acid.Update GlobalState (Edit, Category)
setCategoryNotes catId notes' = do
oldNotes <- categoryById catId . notes <<.= toMarkdownBlock notes'
let edit = Edit'SetCategoryNotes catId (oldNotes ^. mdSource) notes'
oldNotes <- categoryById catId . _categoryNotes <<.= toMarkdownBlock notes'
let edit = Edit'SetCategoryNotes catId (markdownBlockSource oldNotes) notes'
(edit,) <$> use (categoryById catId)
setCategoryStatus :: Uid Category -> CategoryStatus -> Acid.Update GlobalState (Edit, Category)
setCategoryStatus catId status' = do
oldStatus <- categoryById catId . status <<.= status'
oldStatus <- categoryById catId . _categoryStatus <<.= status'
let edit = Edit'SetCategoryStatus catId oldStatus status'
(edit,) <$> use (categoryById catId)
@ -445,59 +446,57 @@ changeCategoryEnabledSections
-> Set ItemSection -- ^ Sections to disable
-> Acid.Update GlobalState (Edit, Category)
changeCategoryEnabledSections catId toEnable toDisable = do
categoryById catId . enabledSections %= \sections ->
categoryById catId . _categoryEnabledSections %= \sections ->
(sections <> toEnable) S.\\ toDisable
let edit = Edit'ChangeCategoryEnabledSections catId toEnable toDisable
(edit,) <$> use (categoryById catId)
setItemName :: Uid Item -> Text -> Acid.Update GlobalState (Edit, Item)
setItemName itemId name' = do
oldName <- itemById itemId . name <<.= name'
oldName <- itemById itemId . _itemName <<.= name'
let edit = Edit'SetItemName itemId oldName name'
(edit,) <$> use (itemById itemId)
setItemLink :: Uid Item -> Maybe Url -> Acid.Update GlobalState (Edit, Item)
setItemLink itemId link' = do
oldLink <- itemById itemId . link <<.= link'
oldLink <- itemById itemId . _itemLink <<.= link'
let edit = Edit'SetItemLink itemId oldLink link'
(edit,) <$> use (itemById itemId)
setItemHackage :: Uid Item -> Maybe Text -> Acid.Update GlobalState (Edit, Item)
setItemHackage itemId hackage' = do
oldName <- itemById itemId . hackage <<.= hackage'
oldName <- itemById itemId . _itemHackage <<.= hackage'
let edit = Edit'SetItemHackage itemId oldName hackage'
(edit,) <$> use (itemById itemId)
setItemSummary :: Uid Item -> Text -> Acid.Update GlobalState (Edit, Item)
setItemSummary itemId description' = do
oldDescr <- itemById itemId . summary <<.=
oldDescr <- itemById itemId . _itemSummary <<.=
toMarkdownBlock description'
let edit = Edit'SetItemSummary itemId
(oldDescr ^. mdSource) description'
(markdownBlockSource oldDescr) description'
(edit,) <$> use (itemById itemId)
setItemNotes :: Uid Item -> Text -> Acid.Update GlobalState (Edit, Item)
setItemNotes itemId notes' = do
let pref = "item-notes-" <> uidToText itemId <> "-"
oldNotes <- itemById itemId . notes <<.=
toMarkdownTree pref notes'
let edit = Edit'SetItemNotes itemId (oldNotes ^. mdSource) notes'
oldNotes <- itemById itemId . _itemNotes <<.= toMarkdownTree pref notes'
let edit = Edit'SetItemNotes itemId (markdownTreeSource oldNotes) notes'
(edit,) <$> use (itemById itemId)
setItemEcosystem :: Uid Item -> Text -> Acid.Update GlobalState (Edit, Item)
setItemEcosystem itemId ecosystem' = do
oldEcosystem <- itemById itemId . ecosystem <<.=
toMarkdownBlock ecosystem'
oldEcosystem <- itemById itemId . _itemEcosystem <<.= toMarkdownBlock ecosystem'
let edit = Edit'SetItemEcosystem itemId
(oldEcosystem ^. mdSource) ecosystem'
(markdownBlockSource oldEcosystem) ecosystem'
(edit,) <$> use (itemById itemId)
setTraitContent :: Uid Item -> Uid Trait -> Text -> Acid.Update GlobalState (Edit, Trait)
setTraitContent itemId traitId content' = do
oldContent <- itemById itemId . traitById traitId . content <<.=
oldContent <- itemById itemId . traitById traitId . _traitContent <<.=
toMarkdownInline content'
let edit = Edit'SetTraitContent itemId traitId
(oldContent ^. mdSource) content'
(markdownInlineSource oldContent) content'
(edit,) <$> use (itemById itemId . traitById traitId)
-- delete
@ -508,17 +507,17 @@ deleteCategory catId = do
case mbCategory of
Nothing -> return (Left "category not found")
Just category -> do
mbCategoryPos <- findIndex (hasUid catId) <$> use categories
mbCategoryPos <- findIndex ((== catId) . categoryUid) <$> use _categories
case mbCategoryPos of
Nothing -> return (Left "category not found")
Just categoryPos -> do
categories %= deleteAt categoryPos
categoriesDeleted %= (category:)
_categories %= deleteAt categoryPos
_categoriesDeleted %= (category:)
return (Right (Edit'DeleteCategory catId categoryPos))
deleteItem :: Uid Item -> Acid.Update GlobalState (Either String Edit)
deleteItem itemId = do
catId <- view uid . findCategoryByItem itemId <$> get
catId <- categoryUid . findCategoryByItem itemId <$> get
let categoryLens :: Lens' GlobalState Category
categoryLens = categoryById catId
let itemLens :: Lens' GlobalState Item
@ -527,12 +526,12 @@ deleteItem itemId = do
case mbItem of
Nothing -> return (Left "item not found")
Just item -> do
allItems <- use (categoryLens.items)
case findIndex (hasUid itemId) allItems of
allItems <- use (categoryLens . _categoryItems)
case findIndex ((== itemId) . itemUid) allItems of
Nothing -> return (Left "item not found")
Just itemPos -> do
categoryLens.items %= deleteAt itemPos
categoryLens.itemsDeleted %= (item:)
categoryLens . _categoryItems %= deleteAt itemPos
categoryLens . _categoryItemsDeleted %= (item:)
return (Right (Edit'DeleteItem itemId itemPos))
deleteTrait :: Uid Item -> Uid Trait -> Acid.Update GlobalState (Either String Edit)
@ -545,27 +544,31 @@ deleteTrait itemId traitId = do
Just item -> do
-- Determine whether the trait is a pro or a con, and proceed
-- accordingly
case (find (hasUid traitId) (item^.pros),
find (hasUid traitId) (item^.cons)) of
case (find ((== traitId) . traitUid) (itemPros item),
find ((== traitId) . traitUid) (itemCons item)) of
-- It's in neither group, which means it was deleted. Do nothing.
(Nothing, Nothing) -> return (Left "trait not found")
-- It's a pro
(Just trait, _) -> do
mbTraitPos <- findIndex (hasUid traitId) <$> use (itemLens.pros)
mbTraitPos <-
findIndex ((== traitId) . traitUid) <$>
use (itemLens . _itemPros)
case mbTraitPos of
Nothing -> return (Left "trait not found")
Just traitPos -> do
itemLens.pros %= deleteAt traitPos
itemLens.prosDeleted %= (trait:)
itemLens . _itemPros %= deleteAt traitPos
itemLens . _itemProsDeleted %= (trait:)
return (Right (Edit'DeleteTrait itemId traitId traitPos))
-- It's a con
(_, Just trait) -> do
mbTraitPos <- findIndex (hasUid traitId) <$> use (itemLens.cons)
mbTraitPos <-
findIndex ((== traitId) . traitUid) <$>
use (itemLens . _itemCons)
case mbTraitPos of
Nothing -> return (Left "trait not found")
Just traitPos -> do
itemLens.cons %= deleteAt traitPos
itemLens.consDeleted %= (trait:)
itemLens . _itemCons %= deleteAt traitPos
itemLens . _itemConsDeleted %= (trait:)
return (Right (Edit'DeleteTrait itemId traitId traitPos))
-- other methods
@ -576,8 +579,8 @@ moveItem
-> Acid.Update GlobalState Edit
moveItem itemId up = do
let move = if up then moveUp else moveDown
catId <- view uid . findCategoryByItem itemId <$> get
categoryById catId . items %= move (hasUid itemId)
catId <- categoryUid . findCategoryByItem itemId <$> get
categoryById catId . _categoryItems %= move ((== itemId) . itemUid)
return (Edit'MoveItem itemId up)
moveTrait
@ -590,67 +593,67 @@ moveTrait itemId traitId up = do
-- The trait is only going to be present in one of the lists so let's do it
-- in each list because we're too lazy to figure out whether it's a pro or
-- a con
itemById itemId . pros %= move (hasUid traitId)
itemById itemId . cons %= move (hasUid traitId)
itemById itemId . _itemPros %= move ((== traitId) . traitUid)
itemById itemId . _itemCons %= move ((== traitId) . traitUid)
return (Edit'MoveTrait itemId traitId up)
restoreCategory :: Uid Category -> Int -> Acid.Update GlobalState (Either String ())
restoreCategory catId pos = do
deleted <- use categoriesDeleted
case find (hasUid catId) deleted of
deleted <- use _categoriesDeleted
case find ((== catId) . categoryUid) deleted of
Nothing -> return (Left "category not found in deleted categories")
Just category -> do
categoriesDeleted %= deleteFirst (hasUid catId)
categories %= insertOrAppend pos category
_categoriesDeleted %= deleteFirst ((== catId) . categoryUid)
_categories %= insertOrAppend pos category
return (Right ())
restoreItem :: Uid Item -> Int -> Acid.Update GlobalState (Either String ())
restoreItem itemId pos = do
let ourCategory = any (hasUid itemId) . view itemsDeleted
allCategories <- use (categories <> categoriesDeleted)
let ourCategory = any ((== itemId) . itemUid) . categoryItemsDeleted
allCategories <- use (_categories <> _categoriesDeleted)
case find ourCategory allCategories of
Nothing -> return (Left "item not found in deleted items")
Just category -> do
let item = fromJust (find (hasUid itemId) (category^.itemsDeleted))
let item = fromJust (find ((== itemId) . itemUid) (categoryItemsDeleted category))
let category' = category
& itemsDeleted %~ deleteFirst (hasUid itemId)
& items %~ insertOrAppend pos item
categories . each . filtered ourCategory .= category'
categoriesDeleted . each . filtered ourCategory .= category'
& _categoryItemsDeleted %~ deleteFirst ((== itemId) . itemUid)
& _categoryItems %~ insertOrAppend pos item
_categories . each . filtered ourCategory .= category'
_categoriesDeleted . each . filtered ourCategory .= category'
return (Right ())
restoreTrait :: Uid Item -> Uid Trait -> Int -> Acid.Update GlobalState (Either String ())
restoreTrait itemId traitId pos = do
let getItems = view (items <> itemsDeleted)
ourCategory = any (hasUid itemId) . getItems
allCategories <- use (categories <> categoriesDeleted)
let getItems = view (_categoryItems <> _categoryItemsDeleted)
ourCategory = any ((== itemId) . itemUid) . getItems
allCategories <- use (_categories <> _categoriesDeleted)
case find ourCategory allCategories of
Nothing -> return (Left "item -that the trait belongs to- not found")
Just category -> do
let item = fromJust (find (hasUid itemId) (getItems category))
case (find (hasUid traitId) (item^.prosDeleted),
find (hasUid traitId) (item^.consDeleted)) of
let item = fromJust (find ((== itemId) . itemUid) (getItems category))
case (find ((== traitId) . traitUid) (itemProsDeleted item),
find ((== traitId) . traitUid) (itemConsDeleted item)) of
(Nothing, Nothing) ->
return (Left "trait not found in deleted traits")
(Just trait, _) -> do
let item' = item
& prosDeleted %~ deleteFirst (hasUid traitId)
& pros %~ insertOrAppend pos trait
& _itemProsDeleted %~ deleteFirst ((== traitId) . traitUid)
& _itemPros %~ insertOrAppend pos trait
let category' = category
& items . each . filtered (hasUid itemId) .~ item'
& itemsDeleted . each . filtered (hasUid itemId) .~ item'
categories . each . filtered ourCategory .= category'
categoriesDeleted . each . filtered ourCategory .= category'
& _categoryItems . each . filtered ((== itemId) . itemUid) .~ item'
& _categoryItemsDeleted . each . filtered ((== itemId) . itemUid) .~ item'
_categories . each . filtered ourCategory .= category'
_categoriesDeleted . each . filtered ourCategory .= category'
return (Right ())
(_, Just trait) -> do
let item' = item
& consDeleted %~ deleteFirst (hasUid traitId)
& cons %~ insertOrAppend pos trait
& _itemConsDeleted %~ deleteFirst ((== traitId) . traitUid)
& _itemCons %~ insertOrAppend pos trait
let category' = category
& items . each . filtered (hasUid itemId) .~ item'
& itemsDeleted . each . filtered (hasUid itemId) .~ item'
categories . each . filtered ourCategory .= category'
categoriesDeleted . each . filtered ourCategory .= category'
& _categoryItems . each . filtered ((== itemId) . itemUid) .~ item'
& _categoryItemsDeleted . each . filtered ((== itemId) . itemUid) .~ item'
_categories . each . filtered ourCategory .= category'
_categoriesDeleted . each . filtered ourCategory .= category'
return (Right ())
-- TODO: maybe have a single list of traits with pro/con being signified by
@ -659,7 +662,7 @@ restoreTrait itemId traitId pos = do
getEdit :: Int -> Acid.Query GlobalState (Edit, EditDetails)
getEdit n = do
edits <- view pendingEdits
edits <- view _pendingEdits
case find ((== n) . editId . snd) edits of
Nothing -> error ("no edit with id " ++ show n)
Just edit -> return edit
@ -670,7 +673,7 @@ getEdits
-> Int -- ^ Id of earliest edit
-> Acid.Query GlobalState [(Edit, EditDetails)]
getEdits m n =
filter (\(_, d) -> n <= editId d && editId d <= m) <$> view pendingEdits
filter (\(_, d) -> n <= editId d && editId d <= m) <$> view _pendingEdits
-- | The edit won't be registered if it's vacuous (see 'isVacuousEdit').
registerEdit
@ -679,21 +682,21 @@ registerEdit
-> UTCTime
-> Acid.Update GlobalState ()
registerEdit ed ip date = do
id' <- use editIdCounter
id' <- use _editIdCounter
let details = EditDetails {
editIP = ip,
editDate = date,
editId = id' }
pendingEdits %= ((ed, details):)
editIdCounter += 1
_pendingEdits %= ((ed, details):)
_editIdCounter += 1
removePendingEdit :: Int -> Acid.Update GlobalState (Edit, EditDetails)
removePendingEdit n = do
edits <- use pendingEdits
edits <- use _pendingEdits
case find ((== n) . editId . snd) edits of
Nothing -> error ("no edit with id " ++ show n)
Just edit -> do
pendingEdits %= deleteFirst ((== n) . editId . snd)
_pendingEdits %= deleteFirst ((== n) . editId . snd)
return edit
removePendingEdits
@ -701,51 +704,51 @@ removePendingEdits
-> Int -- ^ Id of earliest edit
-> Acid.Update GlobalState ()
removePendingEdits m n = do
pendingEdits %= filter (\(_, d) -> editId d < n || m < editId d)
_pendingEdits %= filter (\(_, d) -> editId d < n || m < editId d)
setDirty :: Acid.Update GlobalState ()
setDirty = dirty .= True
setDirty = _dirty .= True
unsetDirty :: Acid.Update GlobalState Bool
unsetDirty = dirty <<.= False
unsetDirty = _dirty <<.= False
-- | Retrieves a session by 'SessionID'.
-- Note: This utilizes a "wrapper" around Spock.Session, 'GuideSession'.
loadSession :: SessionId -> Acid.Query GlobalState (Maybe GuideSession)
loadSession key = view (sessionStore . at key)
loadSession key = view (_sessionStore . at key)
-- | Stores a session object.
-- Note: This utilizes a "wrapper" around Spock.Session, 'GuideSession'.
storeSession :: GuideSession -> Acid.Update GlobalState ()
storeSession sess = do
sessionStore %= M.insert (sess ^. sess_id) sess
_sessionStore %= M.insert (sess ^. sess_id) sess
setDirty
-- | Deletes a session by 'SessionID'.
-- Note: This utilizes a "wrapper" around Spock.Session, 'GuideSession'.
deleteSession :: SessionId -> Acid.Update GlobalState ()
deleteSession key = do
sessionStore %= M.delete key
_sessionStore %= M.delete key
setDirty
-- | Retrieves all sessions.
-- Note: This utilizes a "wrapper" around Spock.Session, 'GuideSession'.
getSessions :: Acid.Query GlobalState [GuideSession]
getSessions = do
m <- view sessionStore
m <- view _sessionStore
return . map snd $ M.toList m
-- | Retrieves a user by their unique identifier.
getUser :: Uid User -> Acid.Query GlobalState (Maybe User)
getUser key = view (users . at key)
getUser key = view (_users . at key)
-- | Creates a user, maintaining unique constraints on certain fields.
createUser :: User -> Acid.Update GlobalState Bool
createUser user = do
m <- toList <$> use users
if all (canCreateUser user) (m ^.. each)
m :: [User] <- toList <$> use _users
if all (canCreateUser user) m
then do
users %= M.insert (user ^. userID) user
_users %= M.insert (userID user) user
return True
else
return False
@ -753,21 +756,21 @@ createUser user = do
-- | Remove a user completely. Unsets all user sessions with this user ID.
deleteUser :: Uid User -> Acid.Update GlobalState ()
deleteUser key = do
users %= M.delete key
_users %= M.delete key
logoutUserGlobally key
setDirty
deleteAllUsers :: Acid.Update GlobalState ()
deleteAllUsers = do
mapM_ logoutUserGlobally . M.keys =<< use users
users .= mempty
mapM_ logoutUserGlobally . M.keys =<< use _users
_users .= mempty
setDirty
-- | Given an email address and a password, return the user if it exists
-- and the password is correct.
loginUser :: Text -> ByteString -> Acid.Query GlobalState (Either String User)
loginUser email password = do
matches <- filter (\u -> u ^. userEmail == email) . toList <$> view users
matches <- filter (\u -> userEmail u == email) . toList <$> view _users
case matches of
[user] ->
if verifyUser user password
@ -779,14 +782,14 @@ loginUser email password = do
-- | Global logout of all of a user's active sessions
logoutUserGlobally :: Uid User -> Acid.Update GlobalState ()
logoutUserGlobally key = do
sessions <- use sessionStore
sessions <- use _sessionStore
for_ (M.toList sessions) $ \(sessID, sess) -> do
when ((sess ^. sess_data.sessionUserID) == Just key) $ do
sessionStore . ix sessID . sess_data . sessionUserID .= Nothing
_sessionStore . ix sessID . sess_data . sessionUserID .= Nothing
-- | Retrieve all users with the 'userIsAdmin' field set to True.
getAdminUsers :: Acid.Query GlobalState [User]
getAdminUsers = filter (^. userIsAdmin) . toList <$> view users
getAdminUsers = filter userIsAdmin . toList <$> view _users
-- | Populate the database with info from the public DB.
importPublicDB :: PublicDB -> Acid.Update GlobalState ()

View File

@ -13,43 +13,20 @@
-- The whole site is a list of categories ('Category'). Categories have
-- items ('Item') in them. Items have some sections (fields inside of
-- 'Item'), as well as traits ('Trait').
--
-- It is recommended to use lenses to access fields of various types. All
-- those lenses are exported from this module. Some lenses (like 'uid' and
-- 'name') are overloaded and can be used with many types.
module Guide.Types.Core
(
Trait(..),
TraitLenses(..),
TraitType (..),
ItemKind(..),
hackageName,
ItemSection(..),
Item(..),
pros,
prosDeleted,
cons,
consDeleted,
ecosystem,
hackage,
link,
ItemLenses(..),
CategoryStatus(..),
Category(..),
title,
group_,
status,
enabledSections,
items,
itemsDeleted,
categorySlug,
-- * Overloaded things
uid,
hasUid,
content,
name,
summary,
notes,
created,
CategoryLenses(..),
categorySlug,
)
where
@ -86,19 +63,19 @@ For an explanation of deriveSafeCopySorted, see Note [acid-state].
-- | A trait (pro or con). Traits are stored in items.
data Trait = Trait {
_traitUid :: Uid Trait,
_traitContent :: MarkdownInline }
traitUid :: Uid Trait,
traitContent :: MarkdownInline }
deriving (Show, Generic, Data)
deriveSafeCopySorted 4 'extension ''Trait
makeFields ''Trait
makeClassWithLenses ''Trait
changelog ''Trait (Current 4, Past 3) []
deriveSafeCopySorted 3 'base ''Trait_v3
instance A.ToJSON Trait where
toJSON = A.genericToJSON A.defaultOptions {
A.fieldLabelModifier = over _head toLower . drop (T.length "_trait") }
A.fieldLabelModifier = over _head toLower . drop (T.length "trait") }
-- | ADT for trait type. Traits can be pros (positive traits) and cons
-- (negative traits).
@ -156,7 +133,7 @@ instance Migrate ItemKind where
migrate Other_v2 = Other
-- | Different kinds of sections inside items. This type is only used for
-- '_categoryEnabledSections'.
-- 'categoryEnabledSections'.
data ItemSection
= ItemProsConsSection
| ItemEcosystemSection
@ -176,39 +153,39 @@ instance A.FromJSON ItemSection where
-- | An item (usually a library). Items are stored in categories.
data Item = Item {
_itemUid :: Uid Item, -- ^ Item ID
_itemName :: Text, -- ^ Item title
_itemCreated :: UTCTime, -- ^ When the item was created
_itemHackage :: Maybe Text, -- ^ Package name on Hackage
_itemSummary :: MarkdownBlock, -- ^ Item summary
_itemPros :: [Trait], -- ^ Pros (positive traits)
_itemProsDeleted :: [Trait], -- ^ Deleted pros go here (so that
-- it'd be easy to restore them)
_itemCons :: [Trait], -- ^ Cons (negative traits)
_itemConsDeleted :: [Trait], -- ^ Deleted cons go here
_itemEcosystem :: MarkdownBlock, -- ^ The ecosystem section
_itemNotes :: MarkdownTree, -- ^ The notes section
_itemLink :: Maybe Url -- ^ Link to homepage or something
itemUid :: Uid Item, -- ^ Item ID
itemName :: Text, -- ^ Item title
itemCreated :: UTCTime, -- ^ When the item was created
itemHackage :: Maybe Text, -- ^ Package name on Hackage
itemSummary :: MarkdownBlock, -- ^ Item summary
itemPros :: [Trait], -- ^ Pros (positive traits)
itemProsDeleted :: [Trait], -- ^ Deleted pros go here (so that
-- it'd be easy to restore them)
itemCons :: [Trait], -- ^ Cons (negative traits)
itemConsDeleted :: [Trait], -- ^ Deleted cons go here
itemEcosystem :: MarkdownBlock, -- ^ The ecosystem section
itemNotes :: MarkdownTree, -- ^ The notes section
itemLink :: Maybe Url -- ^ Link to homepage or something
}
deriving (Show, Generic, Data)
deriveSafeCopySorted 13 'extension ''Item
makeFields ''Item
makeClassWithLenses ''Item
changelog ''Item (Current 13, Past 12)
[Removed "_itemGroup_" [t|Maybe Text|] ]
[Removed "itemGroup_" [t|Maybe Text|] ]
deriveSafeCopySorted 12 'extension ''Item_v12
changelog ''Item (Past 12, Past 11)
[Removed "_itemKind" [t|ItemKind|],
Added "_itemHackage" [hs|
case _itemKind of
[Removed "itemKind" [t|ItemKind|],
Added "itemHackage" [hs|
case itemKind of
Library m -> m
Tool m -> m
Other -> Nothing |],
Removed "_itemDescription" [t|MarkdownBlock|],
Added "_itemSummary" [hs|
_itemDescription |] ]
Removed "itemDescription" [t|MarkdownBlock|],
Added "itemSummary" [hs|
itemDescription |] ]
deriveSafeCopySorted 11 'extension ''Item_v11
changelog ''Item (Past 11, Past 10) []
@ -216,7 +193,7 @@ deriveSafeCopySorted 10 'base ''Item_v10
instance A.ToJSON Item where
toJSON = A.genericToJSON A.defaultOptions {
A.fieldLabelModifier = over _head toLower . drop (T.length "_item") }
A.fieldLabelModifier = over _head toLower . drop (T.length "item") }
----------------------------------------------------------------------------
-- Category
@ -254,45 +231,45 @@ instance Migrate CategoryStatus where
-- | A category
data Category = Category {
_categoryUid :: Uid Category,
_categoryTitle :: Text,
categoryUid :: Uid Category,
categoryTitle :: Text,
-- | When the category was created
_categoryCreated :: UTCTime,
categoryCreated :: UTCTime,
-- | The “grandcategory” of the category (“meta”, “basics”, etc)
_categoryGroup_ :: Text,
_categoryStatus :: CategoryStatus,
_categoryNotes :: MarkdownBlock,
categoryGroup :: Text,
categoryStatus :: CategoryStatus,
categoryNotes :: MarkdownBlock,
-- | Items stored in the category
_categoryItems :: [Item],
categoryItems :: [Item],
-- | Items that were deleted from the category. We keep them here to make
-- it easier to restore them
_categoryItemsDeleted :: [Item],
categoryItemsDeleted :: [Item],
-- | Enabled sections in this category. E.g, if this set contains
-- 'ItemNotesSection', then notes will be shown for each item
_categoryEnabledSections :: Set ItemSection
categoryEnabledSections :: Set ItemSection
}
deriving (Show, Generic, Data)
deriveSafeCopySorted 12 'extension ''Category
makeFields ''Category
makeClassWithLenses ''Category
changelog ''Category (Current 12, Past 11)
[Removed "_categoryGroups" [t|Map Text Hue|] ]
[Removed "categoryGroups" [t|Map Text Hue|] ]
deriveSafeCopySorted 11 'extension ''Category_v11
changelog ''Category (Past 11, Past 10)
[Removed "_categoryProsConsEnabled" [t|Bool|],
Removed "_categoryEcosystemEnabled" [t|Bool|],
Removed "_categoryNotesEnabled" [t|Bool|],
Added "_categoryEnabledSections" [hs|
[Removed "categoryProsConsEnabled" [t|Bool|],
Removed "categoryEcosystemEnabled" [t|Bool|],
Removed "categoryNotesEnabled" [t|Bool|],
Added "categoryEnabledSections" [hs|
S.fromList $ concat
[ [ItemProsConsSection | _categoryProsConsEnabled]
, [ItemEcosystemSection | _categoryEcosystemEnabled]
, [ItemNotesSection | _categoryNotesEnabled] ] |] ]
[ [ItemProsConsSection | categoryProsConsEnabled]
, [ItemEcosystemSection | categoryEcosystemEnabled]
, [ItemNotesSection | categoryNotesEnabled] ] |] ]
deriveSafeCopySorted 10 'extension ''Category_v10
changelog ''Category (Past 10, Past 9)
[Added "_categoryNotesEnabled" [hs|True|]]
[Added "categoryNotesEnabled" [hs|True|]]
deriveSafeCopySorted 9 'extension ''Category_v9
changelog ''Category (Past 9, Past 8) []
@ -300,19 +277,11 @@ deriveSafeCopySorted 8 'base ''Category_v8
instance A.ToJSON Category where
toJSON = A.genericToJSON A.defaultOptions {
A.fieldLabelModifier = over _head toLower . drop (T.length "_category") }
A.fieldLabelModifier = over _head toLower . drop (T.length "category") }
-- | Category identifier (used in URLs). E.g. for a category with title
-- “Performance optimization” and UID “t3c9hwzo” the slug would be
-- @performance-optimization-t3c9hwzo@.
categorySlug :: Category -> Text
categorySlug category =
format "{}-{}" (makeSlug (category^.title)) (category^.uid)
----------------------------------------------------------------------------
-- Utils
----------------------------------------------------------------------------
-- | A useful predicate; @hasUid x@ compares given object's UID with @x@.
hasUid :: HasUid a (Uid u) => Uid u -> a -> Bool
hasUid u x = x^.uid == u
format "{}-{}" (makeSlug (categoryTitle category)) (categoryUid category)

View File

@ -5,16 +5,12 @@
-- | A type for users. Currently unused.
module Guide.Types.User
(
User,
userID,
userName,
userEmail,
userPassword,
userIsAdmin,
User(..),
UserLenses(..),
makeUser,
verifyUser,
canCreateUser,
PublicUser,
PublicUser(..),
userToPublic,
publicUserToUser
)
@ -34,20 +30,20 @@ import Guide.Utils
data User = User {
-- | Unique, pseudorandom identifier for user.
_userID :: Uid User,
userID :: Uid User,
-- | Unique username for user.
_userName :: Text,
userName :: Text,
-- | Unique email address for user.
_userEmail :: Text,
userEmail :: Text,
-- | Scrypt generated password field, contains salt + hash.
_userPassword :: Maybe ByteString,
userPassword :: Maybe ByteString,
-- | Flag set if user is an administrator.
_userIsAdmin :: Bool
userIsAdmin :: Bool
}
deriving (Show)
deriveSafeCopySorted 0 'base ''User
makeLenses ''User
makeClassWithLenses ''User
-- | Creates a user object with an SCrypt encrypted password.
makeUser :: MonadIO m => Text -> Text -> ByteString -> m User
@ -55,29 +51,29 @@ makeUser username email password = do
encPass <- liftIO $ encryptPassIO' (Pass password)
userid <- randomLongUid
return User {
_userID = userid,
_userName = username,
_userEmail = email,
_userPassword = Just $ getEncryptedPass encPass,
_userIsAdmin = False }
userID = userid,
userName = username,
userEmail = email,
userPassword = Just $ getEncryptedPass encPass,
userIsAdmin = False }
-- | Verifies a given password corresponds to a user's encrypted password.
verifyUser :: User -> ByteString -> Bool
verifyUser user password =
case user ^. userPassword of
case userPassword user of
Just encPass -> verifyPass' (Pass password) (EncryptedPass encPass)
Nothing -> False
-- | Looks at two users, and returns true if all unique fields are different.
canCreateUser :: User -> User -> Bool
canCreateUser userFoo userBar =
all (\f -> f userFoo userBar) fieldTests
where
fieldNotEq field a b = a ^. field /= b ^. field
fieldTests = [
fieldNotEq userID,
fieldNotEq userName,
fieldNotEq userEmail ]
canCreateUser $(fieldsPrefixed "a_" 'User) $(fieldsPrefixed "b_" 'User) =
a_userID /= b_userID &&
a_userName /= b_userName &&
a_userEmail /= b_userEmail
where
-- Ignored fields
_ = (a_userIsAdmin, b_userIsAdmin)
_ = (a_userPassword, b_userPassword)
-- | 'PublicUser' contains all safe User data.
-- Removed from 'User':
@ -93,21 +89,24 @@ deriveSafeCopySorted 0 'base ''PublicUser
-- | Converts 'User' to 'PublicUser' type.
userToPublic :: User -> PublicUser
userToPublic User{..} =
userToPublic $(fields 'User) =
PublicUser {
publicUserID = _userID,
publicUserName = _userName,
publicUserEmail = _userEmail,
publicUserIsAdmin = _userIsAdmin
publicUserID = userID,
publicUserName = userName,
publicUserEmail = userEmail,
publicUserIsAdmin = userIsAdmin
}
where
-- Ignored fields
_ = userPassword
-- | Converts 'PublicUser' to 'User' filling password with Nothing.
publicUserToUser :: PublicUser -> User
publicUserToUser PublicUser{..} =
publicUserToUser $(fields 'PublicUser) =
User {
_userID = publicUserID,
_userName = publicUserName,
_userEmail = publicUserEmail,
_userPassword = Nothing,
_userIsAdmin = publicUserIsAdmin
userID = publicUserID,
userName = publicUserName,
userEmail = publicUserEmail,
userPassword = Nothing,
userIsAdmin = publicUserIsAdmin
}

View File

@ -515,6 +515,15 @@ dumpSplices x = do
-- will warn on all unused fields. Thus 'fields' brings safety whenever you
-- want to guarantee that a certain function uses all fields of @Foo@.
--
-- To explicitly ignore a field, match it against @_@:
--
-- @
-- f $(fields 'Foo) = ...
-- where
-- -- Ignored fields
-- _ = (fooUselessField1, fooUselessField2)
-- @
--
-- Usage examples include @ToJSON@ instances and various encoders in
-- general:
--

View File

@ -212,7 +212,7 @@ renderAdmin globalState = do
button "Create checkpoint" [uid_ buttonUid] $
JS.createCheckpoint [JS.selectUid buttonUid]
div_ [id_ "edits"] $
renderEdits globalState (map (,Nothing) (globalState ^. pendingEdits))
renderEdits globalState (map (,Nothing) (pendingEdits globalState))
-- | Group edits by IP and render them.
renderEdits
@ -267,44 +267,45 @@ renderEdit globalState edit = do
quote a = "" *> a <* ""
-- We're searching for everything (items/categories) both in normal lists
-- and in lists of deleted things. Just in case.
let allCategories = globalState^.categories ++
globalState^.categoriesDeleted
let findCategory catId = fromMaybe err (find (hasUid catId) allCategories)
let allCategories = categories globalState ++
categoriesDeleted globalState
let findCategory catId = fromMaybe err (find ((== catId) . categoryUid) allCategories)
where
err = error ("renderEdit: couldn't find category with uid = " ++
toString (uidToText catId))
let findItem itemId = (category, item)
where
getItems = view (items <> itemsDeleted)
ourCategory = any (hasUid itemId) . getItems
getItems = view (_categoryItems <> _categoryItemsDeleted)
ourCategory = any ((== itemId) . itemUid) . getItems
err = error ("renderEdit: couldn't find item with uid = " ++
toString (uidToText itemId))
category = fromMaybe err (find ourCategory allCategories)
item = fromJust (find (hasUid itemId) (getItems category))
item = fromJust (find ((== itemId) . itemUid) (getItems category))
let findTrait itemId traitId = (category, item, trait)
where
(category, item) = findItem itemId
getTraits = view (cons <> consDeleted <> pros <> prosDeleted)
getTraits = view (_itemCons <> _itemConsDeleted <>
_itemPros <> _itemProsDeleted)
err = error ("renderEdit: couldn't find trait with uid = " ++
toString (uidToText traitId))
trait = fromMaybe err (find (hasUid traitId) (getTraits item))
trait = fromMaybe err (find ((== traitId) . traitUid) (getTraits item))
let printCategory catId = do
let category = findCategory catId
quote $ a_ [href_ (categoryLink category)] $
toHtml (category ^. title)
quote $ a_ [href_ (mkCategoryLink category)] $
toHtml (categoryTitle category)
let printItem itemId = do
let (category, item) = findItem itemId
quote $ a_ [href_ (itemLink category item)] $
toHtml (item ^. name)
quote $ a_ [href_ (mkItemLink category item)] $
toHtml (itemName item)
let printCategoryWithItems catId = do
let category = findCategory catId
quote $ toHtml (category ^. title)
let catItems = category ^. items
quote $ toHtml (categoryTitle category)
let catItems = categoryItems category
toHtml $ " with " ++ show (length catItems) ++ " items:"
ul_ $
for_ catItems $ \item ->
li_ $ toHtml (item ^. name)
li_ $ toHtml (itemName item)
case edit of
-- Add
@ -385,7 +386,7 @@ renderEdit globalState edit = do
Edit'SetTraitContent itemId _traitId oldContent newContent -> do
p_ $ (if T.null oldContent then "added" else "changed") >>
" trait of item " >> printItem itemId >>
" from category " >> printCategory (findItem itemId ^. _1.uid)
" from category " >> printCategory (findItem itemId ^. _1 . _categoryUid)
renderDiff oldContent newContent
-- Delete
@ -393,12 +394,12 @@ renderEdit globalState edit = do
"deleted category " >> printCategoryWithItems catId
Edit'DeleteItem itemId _pos -> p_ $ do
let (category, item) = findItem itemId
"deleted item " >> quote (toHtml (item^.name))
" from category " >> quote (toHtml (category^.title))
"deleted item " >> quote (toHtml (itemName item))
" from category " >> quote (toHtml (categoryTitle category))
Edit'DeleteTrait itemId traitId _pos -> do
let (_, item, trait) = findTrait itemId traitId
p_ $ "deleted trait from item " >> quote (toHtml (item^.name))
pre_ $ code_ $ toHtml $ trait^.content
p_ $ "deleted trait from item " >> quote (toHtml (itemName item))
pre_ $ code_ $ toHtml $ traitContent trait
-- Other
Edit'MoveItem itemId direction -> p_ $ do
@ -406,9 +407,9 @@ renderEdit globalState edit = do
if direction then " up" else " down"
Edit'MoveTrait itemId traitId direction -> do
let (_, item, trait) = findTrait itemId traitId
p_ $ "moved trait of item " >> quote (toHtml (item^.name)) >>
p_ $ "moved trait of item " >> quote (toHtml (itemName item)) >>
if direction then " up" else " down"
pre_ $ code_ $ toHtml $ trait^.content
pre_ $ code_ $ toHtml $ traitContent trait
renderDiff :: Monad m => Text -> Text -> HtmlT m ()
renderDiff old new =
@ -473,7 +474,7 @@ renderHaskellRoot globalState mbSearchQuery =
autocomplete_ "off",
onEnter $ JS.addCategoryAndRedirect [inputValue] ]
case mbSearchQuery of
Nothing -> renderCategoryList (globalState^.categories)
Nothing -> renderCategoryList (categories globalState)
Just query' -> renderSearchResults (search query' globalState)
-- TODO: maybe add a button like “give me random category that is
-- unfinished”
@ -483,7 +484,7 @@ renderCategoryPage
:: (MonadIO m, MonadReader Config m)
=> Category -> HtmlT m ()
renderCategoryPage category = do
wrapPage (category^.title <> " Haskell Aelve Guide") $ do
wrapPage (categoryTitle category <> " Haskell Aelve Guide") $ do
onPageLoad $ JS.expandHash ()
haskellHeader
renderNoScriptWarning
@ -520,9 +521,9 @@ wrapPage pageTitle' page = doctypehtml_ $ do
meta_ [name_ "viewport",
content_ "width=device-width, initial-scale=1.0, user-scalable=yes"]
link_ [rel_ "icon", href_ "/favicon.ico"]
googleToken <- _googleToken <$> lift ask
unless (T.null googleToken) $
meta_ [name_ "google-site-verification", content_ googleToken]
token <- googleToken <$> lift ask
unless (T.null token) $
meta_ [name_ "google-site-verification", content_ token]
-- Report all Javascript errors with alerts
script_ [text|
window.onerror = function (msg, url, lineNo, columnNo, error) {
@ -590,35 +591,35 @@ renderSearch mbSearchQuery =
renderCategoryList :: forall m. MonadIO m => [Category] -> HtmlT m ()
renderCategoryList allCats =
div_ [id_ "categories"] $
for_ (groupWith (view group_) allCats) $ \catsInGroup ->
for_ (groupWith categoryGroup allCats) $ \catsInGroup ->
div_ [class_ "category-group"] $ do
-- Grandcategory name
h2_ $ toHtml (catsInGroup^?!_head.group_)
h2_ $ toHtml (categoryGroup (head catsInGroup))
-- Finished categories
do let cats = filter ((== CategoryFinished) . view status) catsInGroup
do let cats = filter ((== CategoryFinished) . categoryStatus) catsInGroup
unless (null cats) $
div_ [class_ "categories-finished"] $ do
mapM_ mkCategoryLink cats
mapM_ mkCategoryLinkHtml cats
-- In-progress categories, separated with commas
do let cats = filter ((== CategoryWIP) . view status) catsInGroup
do let cats = filter ((== CategoryWIP) . categoryStatus) catsInGroup
unless (null cats) $
div_ [class_ "categories-wip"] $ do
h3_ "In progress"
p_ $ sequence_ $ intersperse ", " $
map mkCategoryLink cats
map mkCategoryLinkHtml cats
-- Stub categories, separated with commas
do let cats = filter ((== CategoryStub) . view status) catsInGroup
do let cats = filter ((== CategoryStub) . categoryStatus) catsInGroup
unless (null cats) $
div_ [class_ "categories-stub"] $ do
h3_ "To be written"
p_ $ sequence_ $ intersperse ", " $
map mkCategoryLink cats
map mkCategoryLinkHtml cats
where
-- TODO: this link shouldn't be absolute [absolute-links]
mkCategoryLink :: Category -> HtmlT m ()
mkCategoryLink category =
a_ [class_ "category-link", href_ (categoryLink category)] $
toHtml (category^.title)
mkCategoryLinkHtml :: Category -> HtmlT m ()
mkCategoryLinkHtml category =
a_ [class_ "category-link", href_ (mkCategoryLink category)] $
toHtml (categoryTitle category)
-- | Render a <div> with search results.
renderSearchResults :: Monad m => [SearchResult] -> HtmlT m ()
@ -632,27 +633,29 @@ renderSearchResult r = do
div_ [class_ "search-result"] $
case r of
SRCategory cat -> do
a_ [class_ "category-link", href_ (categoryLink cat)] $
toHtml (cat^.title)
a_ [class_ "category-link", href_ (mkCategoryLink cat)] $
toHtml (categoryTitle cat)
div_ [class_ "category-description notes-like"] $
toHtml (extractPreface $ toMarkdownTree "" $ cat^.notes.mdSource)
toHtml $ extractPreface $
toMarkdownTree "" $
markdownBlockSource (categoryNotes cat)
SRItem cat item -> do
a_ [class_ "category-link in-item-sr", href_ (categoryLink cat)] $
toHtml (cat^.title)
a_ [class_ "category-link in-item-sr", href_ (mkCategoryLink cat)] $
toHtml (categoryTitle cat)
span_ [class_ "breadcrumb"] "»"
a_ [class_ "item-link", href_ (itemLink cat item)] $
toHtml (item^.name)
a_ [class_ "item-link", href_ (mkItemLink cat item)] $
toHtml (itemName item)
div_ [class_ "description notes-like"] $
toHtml (item^.summary)
toHtml (itemSummary item)
SRItemEcosystem cat item -> do
a_ [class_ "category-link in-item-sr", href_ (categoryLink cat)] $
toHtml (cat^.title)
a_ [class_ "category-link in-item-sr", href_ (mkCategoryLink cat)] $
toHtml (categoryTitle cat)
span_ [class_ "breadcrumb"] "»"
a_ [class_ "item-link", href_ (itemLink cat item)] $
toHtml (item^.name)
a_ [class_ "item-link", href_ (mkItemLink cat item)] $
toHtml (itemName item)
span_ [class_ "item-link-addition"] "'s ecosystem"
div_ [class_ "ecosystem notes-like"] $
toHtml (item^.ecosystem)
toHtml (itemEcosystem item)
{- Note [enabled sections]
~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -820,24 +823,24 @@ renderArchivalStatus = \case
-- | Find all links in content, along with a human-readable description of
-- where each link is located.
findLinks :: GlobalState -> [(Url, Text)]
findLinks = concatMap findLinksCategory . view categories
findLinks = concatMap findLinksCategory . view _categories
-- | Find all links in a single category.
findLinksCategory :: Category -> [(Url, Text)]
findLinksCategory cat =
[(url, cat^.title <> " (category notes)")
| url <- findLinksMD (cat^.notes)] ++
[(url, cat^.title <> " / " <> item^.name)
| item <- cat^.items
[(url, categoryTitle cat <> " (category notes)")
| url <- findLinksMD (categoryNotes cat)] ++
[(url, categoryTitle cat <> " / " <> itemName item)
| item <- categoryItems cat
, url <- findLinksItem item]
-- | Find all links in a single item.
findLinksItem :: Item -> [Url]
findLinksItem item = findLinksMD item' ++ maybeToList (item^.link)
findLinksItem item = findLinksMD item' ++ maybeToList (itemLink item)
where
-- we don't want to find any links in deleted traits
item' = item & prosDeleted .~ []
& consDeleted .~ []
item' = item & _itemProsDeleted .~ []
& _itemConsDeleted .~ []
-- | Find all Markdown links in /any/ structure, using generics.
findLinksMD :: Data a => a -> [Url]

View File

@ -53,7 +53,7 @@ loginView user = do
div_ $ do
-- TODO: Make nicer.
"You are registered and logged in as "
toHtml (user ^. userName)
toHtml (userName user)
renderLogin :: (MonadIO m, MonadReader Config m) => HtmlT m () -> HtmlT m ()
renderLogin content = do

View File

@ -69,7 +69,7 @@ registerView user = do
div_ $ do
-- TODO: Make nicer.
"You are registered and logged in as "
toHtml (user ^. userName)
toHtml (userName user)
renderRegister :: (MonadIO m, MonadReader Config m) => HtmlT m () -> HtmlT m ()
renderRegister content = do

View File

@ -43,13 +43,13 @@ renderCategory category = div_ [class_ "category", id_ (categoryNodeId category)
renderCategoryInfo category
renderCategoryNotes category
itemsNode <- div_ [class_ "items"] $ do
mapM_ (renderItem category) (category^.items)
mapM_ (renderItem category) (categoryItems category)
thisNode
textInput [
class_ " add-item ",
placeholder_ "add an item",
autocomplete_ "off",
onEnter $ JS.addItem (itemsNode, category^.uid, inputValue) <>
onEnter $ JS.addItem (itemsNode, categoryUid category, inputValue) <>
clearInput ]
----------------------------------------------------------------------------
@ -60,7 +60,7 @@ renderCategory category = div_ [class_ "category", id_ (categoryNodeId category)
-- form + possibly status banner).
renderCategoryInfo :: MonadIO m => Category -> HtmlT m ()
renderCategoryInfo category =
let thisId = "category-info-" <> uidToText (category^.uid)
let thisId = "category-info-" <> uidToText (categoryUid category)
this = JS.selectId thisId
in div_ [id_ thisId, class_ "category-info"] $ do
section "normal" [shown, noScriptShown] $ do
@ -68,25 +68,25 @@ renderCategoryInfo category =
-- TODO: this link shouldn't be absolute [absolute-links]
span_ [class_ "controls"] $
a_ [class_ "category-feed",
href_ ("/haskell/feed/category/" <> uidToText (category^.uid))] $
href_ ("/haskell/feed/category/" <> uidToText (categoryUid category))] $
img_ [src_ "/rss-alt.svg",
alt_ "category feed", title_ "category feed"]
a_ [href_ (categoryLink category), class_ "category-title"] $
toHtml (category^.title)
a_ [href_ (mkCategoryLink category), class_ "category-title"] $
toHtml (categoryTitle category)
emptySpan "1em"
span_ [class_ "group"] $
toHtml (category^.group_)
toHtml (categoryGroup category)
emptySpan "1em"
textButton "edit" $
JS.switchSection (this, "editing" :: Text)
emptySpan "1em"
textButton "delete" $
JS.deleteCategoryAndRedirect [category^.uid]
JS.deleteCategoryAndRedirect [categoryUid category]
renderCategoryStatus category
section "editing" [] $ do
let formSubmitHandler formNode =
JS.submitCategoryInfo (this, category^.uid, formNode)
JS.submitCategoryInfo (this, categoryUid category, formNode)
form_ [onFormSubmit formSubmitHandler] $ do
-- All inputs have "autocomplete = off" thanks to
-- <http://stackoverflow.com/q/8311455>
@ -94,40 +94,40 @@ renderCategoryInfo category =
"Title" >> br_ []
input_ [type_ "text", name_ "title",
autocomplete_ "off",
value_ (category^.title)]
value_ (categoryTitle category)]
br_ []
label_ $ do
"Group" >> br_ []
input_ [type_ "text", name_ "group",
autocomplete_ "off",
value_ (category^.group_)]
value_ (categoryGroup category)]
br_ []
label_ $ do
"Status" >> br_ []
select_ [name_ "status", autocomplete_ "off"] $ do
option_ [value_ "finished"] "Complete"
& selectedIf (category^.status == CategoryFinished)
& selectedIf (categoryStatus category == CategoryFinished)
option_ [value_ "wip"] "Work in progress"
& selectedIf (category^.status == CategoryWIP)
& selectedIf (categoryStatus category == CategoryWIP)
option_ [value_ "stub"] "Stub"
& selectedIf (category^.status == CategoryStub)
& selectedIf (categoryStatus category == CategoryStub)
br_ []
label_ $ do
input_ [type_ "checkbox", name_ "pros-cons-enabled",
autocomplete_ "off"]
& checkedIf (ItemProsConsSection `elem` category^.enabledSections)
& checkedIf (ItemProsConsSection `elem` categoryEnabledSections category)
"Pros/cons enabled"
br_ []
label_ $ do
input_ [type_ "checkbox", name_ "ecosystem-enabled",
autocomplete_ "off"]
& checkedIf (ItemEcosystemSection `elem` category^.enabledSections)
& checkedIf (ItemEcosystemSection `elem` categoryEnabledSections category)
"“Ecosystem” field enabled"
br_ []
label_ $ do
input_ [type_ "checkbox", name_ "notes-enabled",
autocomplete_ "off"]
& checkedIf (ItemNotesSection `elem` category^.enabledSections)
& checkedIf (ItemNotesSection `elem` categoryEnabledSections category)
"“Notes” field enabled"
br_ []
input_ [type_ "submit", value_ "Save", class_ "save"]
@ -138,7 +138,7 @@ renderCategoryInfo category =
-- unfinished category.
renderCategoryStatus :: MonadIO m => Category -> HtmlT m ()
renderCategoryStatus category = do
case category^.status of
case categoryStatus category of
CategoryFinished -> return ()
CategoryWIP -> catBanner $ do
"This category is a work in progress"
@ -153,14 +153,14 @@ renderCategoryStatus category = do
-- | Render category notes (or “description”).
renderCategoryNotes :: MonadIO m => Category -> HtmlT m ()
renderCategoryNotes category =
let thisId = "category-notes-" <> uidToText (category^.uid)
let thisId = "category-notes-" <> uidToText (categoryUid category)
this = JS.selectId thisId
in div_ [id_ thisId, class_ "category-notes"] $ do
section "normal" [shown, noScriptShown] $ do
div_ [class_ "notes-like"] $ do
if markdownNull (category^.notes)
if markdownBlockSource (categoryNotes category) == ""
then p_ "write something here!"
else toHtml (category^.notes)
else toHtml (categoryNotes category)
textButton "edit description" $
JS.switchSection (this, "editing" :: Text) <>
JS.focusOn [(this `JS.selectSection` "editing")
@ -168,14 +168,17 @@ renderCategoryNotes category =
JS.selectClass "editor"]
section "editing" [] $ do
contents <- if markdownNull (category^.notes)
contents <- if markdownBlockSource (categoryNotes category) == ""
then liftIO $ toMarkdownBlock <$>
T.readFile "static/category-notes-template.md"
else return (category^.notes)
else return (categoryNotes category)
markdownEditor
10 -- rows
contents
(\val -> JS.withThis JS.submitCategoryNotes
(this, category^.uid, category^.notes.mdSource, val))
(this,
categoryUid category,
markdownBlockSource (categoryNotes category),
val))
(JS.withThis JS.switchSection (this, "normal" :: Text))
"or press Ctrl+Enter to save"

View File

@ -58,13 +58,13 @@ renderItem category item =
div_ [class_ "item-body", style_ ("background-color:#F0F0F0")] $ do
-- See Note [enabled sections]
renderItemDescription item
hiddenIf (ItemProsConsSection `notElem` category^.enabledSections) $
hiddenIf (ItemProsConsSection `notElem` categoryEnabledSections category) $
div_ [class_ "pros-cons-wrapper"] $
renderItemTraits item
hiddenIf (ItemEcosystemSection `notElem` category^.enabledSections) $
hiddenIf (ItemEcosystemSection `notElem` categoryEnabledSections category) $
div_ [class_ "ecosystem-wrapper"] $
renderItemEcosystem item
hiddenIf (ItemNotesSection `notElem` category^.enabledSections) $
hiddenIf (ItemNotesSection `notElem` categoryEnabledSections category) $
div_ [class_ "notes-wrapper"] $
renderItemNotes category item
@ -74,21 +74,21 @@ renderItemForFeed
=> Category -> Item -> HtmlT m ()
renderItemForFeed category item = do
h1_ $ renderItemTitle item
unless (markdownNull (item^.summary)) $
toHtml (item^.summary)
when (ItemProsConsSection `elem` category^.enabledSections) $ do
unless (markdownBlockSource (itemSummary item) == "") $
toHtml (itemSummary item)
when (ItemProsConsSection `elem` categoryEnabledSections category) $ do
h2_ "Pros"
ul_ $ mapM_ (p_ . li_ . toHtml . view content) (item^.pros)
ul_ $ mapM_ (p_ . li_ . toHtml . traitContent) (itemPros item)
h2_ "Cons"
ul_ $ mapM_ (p_ . li_ . toHtml . view content) (item^.cons)
when (ItemEcosystemSection `elem` category^.enabledSections) $ do
unless (markdownNull (item^.ecosystem)) $ do
ul_ $ mapM_ (p_ . li_ . toHtml . traitContent) (itemCons item)
when (ItemEcosystemSection `elem` categoryEnabledSections category) $ do
unless (markdownBlockSource (itemEcosystem item) == "") $ do
h2_ "Ecosystem"
toHtml (item^.ecosystem)
toHtml (itemEcosystem item)
-- TODO: include .notes-like style here? otherwise the headers are too big
unless (markdownNull (item^.notes)) $ do
unless (markdownTreeSource (itemNotes item) == "") $ do
h2_ "Notes"
toHtml (item^.notes)
toHtml (itemNotes item)
----------------------------------------------------------------------------
-- Helpers
@ -109,8 +109,8 @@ renderItemInfo cat item =
mustache "item-info" $ A.object [
"category" A..= cat,
"item" A..= item,
"link_to_item" A..= itemLink cat item,
"hackage" A..= (item^.hackage) ]
"link_to_item" A..= mkItemLink cat item,
"hackage" A..= itemHackage item ]
-- | Render item description.
renderItemDescription :: MonadIO m => Item -> HtmlT m ()
@ -120,7 +120,7 @@ renderItemDescription item = mustache "item-description" $
-- | Render the “ecosystem” section.
renderItemEcosystem :: MonadIO m => Item -> HtmlT m ()
renderItemEcosystem item =
let thisId = "item-ecosystem-" <> uidToText (item^.uid)
let thisId = "item-ecosystem-" <> uidToText (itemUid item)
this = JS.selectId thisId
in div_ [id_ thisId, class_ "item-ecosystem"] $ do
@ -134,8 +134,8 @@ renderItemEcosystem item =
`JS.selectChildren`
JS.selectClass "editor"]
div_ [class_ "notes-like"] $ do
unless (markdownNull (item^.ecosystem)) $
toHtml (item^.ecosystem)
unless (markdownBlockSource (itemEcosystem item) == "") $
toHtml (itemEcosystem item)
section "editing" [] $ do
strong_ "Ecosystem"
@ -145,9 +145,9 @@ renderItemEcosystem item =
JS.switchSection (this, "normal" :: Text)
markdownEditor
3 -- rows
(item^.ecosystem)
(itemEcosystem item)
(\val -> JS.withThis JS.submitItemEcosystem
(this, item^.uid, item^.ecosystem.mdSource, val))
(this, itemUid item, markdownBlockSource (itemEcosystem item), val))
(JS.withThis JS.switchSection (this, "normal" :: Text))
"or press Ctrl+Enter to save"
@ -171,14 +171,14 @@ renderItemTraits item = div_ [class_ "item-traits"] $ do
-- and only <li> elements can be children of <ul>
listUid <- randomLongUid
ul_ [uid_ listUid] $
mapM_ (renderTrait (item^.uid)) (item^.pros)
mapM_ (renderTrait (itemUid item)) (itemPros item)
section "editable" [] $ do
smallMarkdownEditor
3 -- rows
(toMarkdownInline "")
-- TODO: clearing the editor should be moved into 'addPro' and
-- done only if the request succeeds
(\val -> JS.withThis JS.addPro (JS.selectUid listUid, item^.uid, val))
(\val -> JS.withThis JS.addPro (JS.selectUid listUid, itemUid item, val))
Nothing
"press Ctrl+Enter or Enter to add"
(Just "add pro") -- placeholder
@ -199,14 +199,14 @@ renderItemTraits item = div_ [class_ "item-traits"] $ do
JS.switchSectionsEverywhere (this, "normal" :: Text)
listUid <- randomLongUid
ul_ [uid_ listUid] $
mapM_ (renderTrait (item^.uid)) (item^.cons)
mapM_ (renderTrait (itemUid item)) (itemCons item)
section "editable" [] $ do
smallMarkdownEditor
3 -- rows
(toMarkdownInline "")
-- TODO: clearing the editor should be moved into 'addCon' and
-- done only if the request succeeds
(\val -> JS.withThis JS.addCon (JS.selectUid listUid, item^.uid, val))
(\val -> JS.withThis JS.addCon (JS.selectUid listUid, itemUid item, val))
Nothing
"press Ctrl+Enter or Enter to add"
(Just "add con") -- placeholder
@ -225,11 +225,11 @@ renderTrait itemUid trait =
renderItemNotes :: MonadIO m => Category -> Item -> HtmlT m ()
renderItemNotes category item = do
-- Don't change this ID, it's used in e.g. 'JS.expandHash'
let thisId = "item-notes-" <> uidToText (item^.uid)
let thisId = "item-notes-" <> uidToText (itemUid item)
this = JS.selectId thisId
editingSectionUid <- randomLongUid
div_ [id_ thisId, class_ "item-notes"] $ do
let notesLink = categoryLink category <> "#" <> thisId
let notesLink = mkCategoryLink category <> "#" <> thisId
a_ [href_ notesLink] $
strong_ "Notes"
@ -237,7 +237,7 @@ renderItemNotes category item = do
renderTree [] = return ()
renderTree xs = ul_ $ do
for_ xs $ \(Node {-(is, id')-} (Heading hMd id') children) -> li_ $ do
let handler = fromJS (JS.expandItemNotes [item^.uid])
let handler = fromJS (JS.expandItemNotes [itemUid item])
-- The link has to be absolute because sometimes we are
-- looking at items from pages different from the proper
-- category pages (e.g. if a search from the main page
@ -247,12 +247,12 @@ renderItemNotes category item = do
-- there's no search (or rather, there is search but it
-- doesn't return items, only categories); however, it might
-- start happening and then it's better to be prepared.
fullLink = categoryLink category <> "#" <> id'
fullLink = mkCategoryLink category <> "#" <> id'
a_ [href_ fullLink, onclick_ handler] $
toHtmlRaw (markdownInlineMdHtml hMd)
toHtmlRaw (markdownInlineHtml hMd)
renderTree children
let renderTOC = do
let toc = item^.notes.mdTOC
let toc = markdownTreeTOC (itemNotes item)
div_ [class_ "notes-toc"] $ do
if null toc
then p_ (emptySpan "1.5em" >> "<notes are empty>")
@ -260,14 +260,14 @@ renderItemNotes category item = do
section "collapsed" [shown] $ do
textButton "expand notes" $
JS.expandItemNotes [item^.uid]
JS.expandItemNotes [itemUid item]
renderTOC
section "expanded" [noScriptShown] $ do
textareaUid <- randomLongUid
contents <- if markdownNull (item^.notes)
contents <- if markdownTreeSource (itemNotes item) == ""
then liftIO $ T.readFile "static/item-notes-template.md"
else return (item^.notes.mdSource)
else return (markdownTreeSource (itemNotes item))
let buttons = do
textButton "collapse notes" $
JS.switchSection (this, "collapsed" :: Text)
@ -278,19 +278,19 @@ renderItemNotes category item = do
this, JS.selectUid editingSectionUid,
textareaUid,
-- See Note [blurb diffing]
markdownNull (item^.notes),
markdownTreeSource (itemNotes item) == "",
contents,
item^.uid) <>
itemUid item) <>
JS.switchSection (this, "editing" :: Text) <>
JS.autosizeTextarea [JS.selectUid textareaUid] <>
JS.focusOn [JS.selectUid textareaUid]
buttons
renderTOC
div_ [class_ "notes-like"] $ do
if markdownNull (item^.notes)
if markdownTreeSource (itemNotes item) == ""
then p_ "add something!"
else toHtml (item^.notes)
unless (markdownNull (item^.notes)) $
else toHtml (itemNotes item)
unless (markdownTreeSource (itemNotes item) == "") $
buttons
-- TODO: [easy] the lower “hide notes” should scroll back to item when
-- the notes are closed (but don't scroll if it's already visible after

View File

@ -128,7 +128,7 @@ headTagDef page = do
meta_ [name_ "viewport",
content_ "width=device-width, initial-scale=1.0, user-scalable=yes"]
link_ [rel_ "icon", href_ "/favicon.ico"]
googleToken <- _googleToken <$> lift ask
googleToken <- googleToken <$> lift ask
unless (T.null googleToken) $
meta_ [name_ "google-site-verification", content_ googleToken]
-- Report all Javascript errors with alerts

View File

@ -19,8 +19,8 @@ module Guide.Views.Utils
selectedIf,
checkedIf,
hiddenIf,
categoryLink,
itemLink,
mkCategoryLink,
mkItemLink,
-- * HTML components
button,
@ -195,7 +195,7 @@ markdownEditor
-> JS -- ^ “Cancel” handler
-> Text -- ^ Instruction (e.g. “press Ctrl+Enter to save”)
-> HtmlT m ()
markdownEditor rows (view mdSource -> src) submit cancel instr = do
markdownEditor rows (markdownBlockSource -> src) submit cancel instr = do
editorUid <- randomLongUid
term "a-editor" [uid_ editorUid,
vBind "init-content" src,
@ -216,7 +216,7 @@ smallMarkdownEditor
-> Text -- ^ Instruction (e.g. “press Enter to add”)
-> Maybe Text -- ^ Placeholder
-> HtmlT m ()
smallMarkdownEditor rows (view mdSource -> src) submit mbCancel instr mbPlaceholder = do
smallMarkdownEditor rows (markdownInlineSource -> src) submit mbCancel instr mbPlaceholder = do
editorUid <- randomLongUid
term "a-editor-mini" ([uid_ editorUid,
vBind "init-content" src,
@ -239,17 +239,17 @@ thisNode = do
return (JS.selectParent (JS.selectUid uid'))
itemNodeId :: Item -> Text
itemNodeId item = format "item-{}" (item^.uid)
itemNodeId item = format "item-{}" (itemUid item)
categoryNodeId :: Category -> Text
categoryNodeId category = format "category-{}" (category^.uid)
categoryNodeId category = format "category-{}" (categoryUid category)
-- TODO: another absolute link to get rid of [absolute-links]
categoryLink :: Category -> Url
categoryLink category = format "/haskell/{}" (categorySlug category)
mkCategoryLink :: Category -> Url
mkCategoryLink category = format "/haskell/{}" (categorySlug category)
itemLink :: Category -> Item -> Url
itemLink category item =
mkItemLink :: Category -> Item -> Url
mkItemLink category item =
format "/haskell/{}#{}" (categorySlug category) (itemNodeId item)
-- See Note [show-hide]; wheh changing these, also look at 'JS.switchSection'.

View File

@ -25,7 +25,7 @@ import Guide.Markdown
tests :: Spec
tests = describe "Markdown" $ do
allMarkdowns $ \convert -> do
it "has mdSource filled accurately" $ do
it "has the source filled accurately" $ do
for_ mdBlockExamples $ \s ->
s `shouldBe` fst (convert s)
it "only has allowed tags" $ do
@ -110,7 +110,7 @@ tests = describe "Markdown" $ do
headingMD = MD.Node Nothing (TEXT "foo") []
foo2MD = MD.Node (Just (PosInfo 7 1 7 1)) PARAGRAPH
[MD.Node Nothing (TEXT "y") []]
(toMarkdownTree "i-" s ^. mdTree) `shouldBe` Document {
markdownTreeStructure (toMarkdownTree "i-" s) `shouldBe` Document {
prefaceAnn = "<p>x</p>\n",
preface = WithSource "x\n\n" [prefaceMD],
sections = [
@ -130,7 +130,7 @@ tests = describe "Markdown" $ do
subForest = [] }]}]}
it "has a correct TOC" $ do
let s = "x\n\n# foo\n\n## foo\n\ny"
(toMarkdownTree "i-" s ^. mdTOC) `shouldBe` [
markdownTreeTOC (toMarkdownTree "i-" s) `shouldBe` [
Node {rootLabel = Heading (toMarkdownInline "# foo\n\n") "i-foo",
subForest = [
Node {rootLabel = Heading (toMarkdownInline "## foo\n\n") "i-foo_",
@ -145,15 +145,15 @@ htmlToText = toText . renderText . toHtml
allMarkdowns :: ((Text -> (Text, Text)) -> Spec) -> Spec
allMarkdowns f = do
describe "inline MD" $
f ((view mdSource &&& htmlToText) . toMarkdownInline)
f ((markdownInlineSource &&& htmlToText) . toMarkdownInline)
blockMarkdowns f
blockMarkdowns :: ((Text -> (Text, Text)) -> Spec) -> Spec
blockMarkdowns f = do
describe "block MD" $
f ((view mdSource &&& htmlToText) . toMarkdownBlock)
f ((markdownBlockSource &&& htmlToText) . toMarkdownBlock)
describe "block+toc MD" $
f ((view mdSource &&& htmlToText) . toMarkdownTree "")
f ((markdownTreeSource &&& htmlToText) . toMarkdownTree "")
mdInlineExamples :: [Text]
mdInlineExamples = [

View File

@ -507,13 +507,13 @@ run :: FilePath -> Spec -> IO ()
run logFile ts = do
-- Config to run spock server.
let config = def {
_baseUrl = "/",
_googleToken = "some-google-token",
_adminPassword = "123",
_discussLink = Just "http://discuss.link",
_cors = False,
_logToStderr = False,
_logToFile = Just logFile
baseUrl = "/",
googleToken = "some-google-token",
adminPassword = "123",
discussLink = Just "http://discuss.link",
cors = False,
logToStderr = False,
logToFile = Just logFile
}
-- Prepere resources.
let prepare = do