car-pool-0.0: src/Main.hs
-- module Main (main) where
import Text.Digestive (Form, View, text, bool, subView, check, (.:))
import Text.Digestive.Blaze.Html5
(inputText, inputCheckbox, inputSubmit, label, form, errorList)
import Text.Digestive.Happstack (runForm)
import qualified Text.Blaze.Html5.Attributes as HA
import qualified Text.Blaze.Html5 as H
import qualified Happstack.Server as Happstack
import qualified Data.Spreadsheet as Spreadsheet
import qualified Control.Monad.Exception.Asynchronous as AsyncExc
import qualified Control.Monad.Trans.State as MS
import Control.Monad.IO.Class (MonadIO, liftIO)
import qualified Data.NonEmpty.Class as NonEmptyC
import qualified Data.NonEmpty as NonEmpty
import qualified Data.Map as Map; import Data.Map (Map)
import qualified Data.Set as Set
import qualified Data.Char as Char
import qualified Data.List as List
import qualified Data.Text as T
import Data.Maybe.HT (toMaybe)
import Data.Maybe (catMaybes)
import Data.Text (Text)
import qualified Data.Foldable as Fold
import Control.Applicative (Applicative, liftA2, pure, (<*>), (<$>))
import Data.Traversable (traverse)
import Data.Foldable (forM_)
import Data.Monoid (mappend, mconcat)
import qualified System.IO as IO
data Place = Place
{ placeName :: Text
, placeContact :: Text
, placeOffer, placeRequest :: Bool
} deriving (Show)
t :: String -> Text
t = T.pack
h :: String -> H.Html
h = H.toHtml
idAnyTown, idTowns, idOther, idMail,
idOffer, idRequest, idOfferRequest :: Text
idAnyTown = t"anytown"
idTowns = t"town"
idOther = t"other"
idMail = t"mail"
idOffer = t"offer"
idRequest = t"request"
idOfferRequest = t"or"
idFromTown :: Text -> Text
idFromTown = T.map Char.toLower . T.filter Char.isAlpha
placeForm :: Monad m => [Text] -> Form Text m [Place]
placeForm oldTowns =
pure
(\newTowns mail (offer, request) ->
map (\town -> Place town mail offer request) newTowns)
<*> townsForm oldTowns
<*> idMail .: check (t"Bitte E-Mail oder Telefonnummer angeben.") checkMail (text Nothing)
<*> offerRequestForm
townsForm :: Monad m => [Text] -> Form Text m [Text]
townsForm towns =
(idAnyTown .:) $
check (t"Bitte mindestens eine Stadt ausw\228hlen.") (not . null) $
fmap catMaybes $
pure (:)
<*> idOther .:
fmap (\other -> toMaybe (not $ T.null other) other) (text Nothing)
<*> idTowns .:
traverse
(\town -> flip toMaybe town <$> (idFromTown town .: bool Nothing))
towns
offerRequestForm :: Monad m => Form Text m (Bool, Bool)
offerRequestForm =
(idOfferRequest .:) $
check (t"Bitte mindestens Gebot oder Gesuch w\228hlen.") (uncurry (||)) $
liftA2 (,)
(idOffer .: bool Nothing)
(idRequest .: bool Nothing)
checkMail :: Text -> Bool
checkMail = not . T.null
type Parser a = MS.State [String] a
parseField :: a -> (String -> a) -> Parser a
parseField deflt convert =
MS.state $ \xt ->
case xt of
[] -> (deflt, [])
x:xs -> (convert x, xs)
runParser :: Parser a -> [String] -> a
runParser = MS.evalState
placeFromLine :: [String] -> Place
placeFromLine =
runParser $
pure Place
<*> parseField T.empty T.pack
<*> parseField T.empty T.pack
<*> parseField False (not . null)
<*> parseField False (not . null)
lineFromPlace :: Place -> [String]
lineFromPlace p =
T.unpack (placeName p) :
T.unpack (placeContact p) :
csvFromBool (placeOffer p) :
csvFromBool (placeRequest p) :
[]
placeView :: View H.Html -> [Text] -> H.Html
placeView view towns = do
H.h2 $ h"Mitfahrgelegenheit"
errorList idAnyTown view
H.table $ do
let tview = subView idAnyTown view
forM_ towns $ \town -> H.tr $ do
let idTown = idFromTown town
sview = subView idTowns tview
H.td $ inputCheckbox idTown sview -- H.! H.align "right"
H.td $ label idTown sview (H.toHtml town)
H.tr $ do
H.td $ label idOther tview (h"andere Stadt: ")
H.td $ inputText idOther tview
errorList idMail view
label idMail view (h"Kontakt: ")
inputText idMail view
H.br
errorList idOfferRequest view
case subView idOfferRequest view of
sview -> do
label idOffer sview (h"Gebot: ")
inputCheckbox idOffer sview
label idRequest sview (h"Gesuch: ")
inputCheckbox idRequest sview
H.br
readCSV ::
([Place] -> Happstack.ServerPart Happstack.Response) ->
Happstack.ServerPart Happstack.Response
readCSV f = do
csv <- liftIO $ readFile csvPath
case Spreadsheet.fromString '"' ',' csv of
AsyncExc.Exceptional mmsg table ->
case mmsg of
Just msg -> do
liftIO $ IO.hPutStrLn IO.stderr msg
Happstack.internalServerError $
Happstack.toResponse "Cannot read database."
Nothing ->
f $ map placeFromLine table
groupPlaces :: [Place] -> Map Text (NonEmpty.T [] Place)
groupPlaces =
Map.fromListWith NonEmptyC.append .
map (\place -> (placeName place, NonEmpty.singleton place))
htmlFromContact :: Text -> H.Html
htmlFromContact contact =
case T.find ('@'==) contact of
Nothing -> H.toHtml contact
Just _ ->
H.a (H.toHtml contact)
H.! (HA.href $ H.toValue $ mappend (t"mailto:") contact)
htmlFromContacts :: Text -> H.Html
htmlFromContacts =
mconcat . List.intersperse (H.toHtml ", ") .
map htmlFromContact . T.split (','==)
htmlFromPlaces :: Map Text (NonEmpty.T [] Place) -> H.Html
htmlFromPlaces places =
H.table $ do
H.tr $ mapM_ (H.th . h) $
"Stadt" : "Kontakt" : "Gebot" : "Gesuch" : []
Fold.sequence_ $ flip Map.mapWithKey places $ \town (NonEmpty.Cons contact contacts) ->
let row place = do
H.td $ htmlFromContacts $ placeContact place
H.td $ htmlFromBool $ placeOffer place
H.td $ htmlFromBool $ placeRequest place
in do H.tr $ do
H.td $ H.toHtml town
row contact
forM_ contacts $ \c -> H.tr $ do
H.td $ h""
row c
site :: Happstack.ServerPart Happstack.Response
site = readCSV $ \places -> do
Happstack.decodeBody $ Happstack.defaultBodyPolicy "/tmp" 4096 4096 4096
let towns = Set.toAscList $ Set.fromList $ map placeName places
r <- runForm (t"carpool") $ placeForm towns
case r of
(view, Nothing) -> do
let htmlView = fmap H.toHtml view
Happstack.ok $ Happstack.toResponse $
template $ do
H.h1 $ h title
htmlFromPlaces $ groupPlaces places
form htmlView (t"/") $ do
placeView htmlView towns
H.br
inputSubmit $ t"Anmelden"
(_, Just newPlaces) -> do
liftIO $ appendFile csvPath $
Spreadsheet.toString '"' ',' $ map lineFromPlace newPlaces
Happstack.ok $ Happstack.toResponse $
template $ do
H.h1 $ h"Anmeldung erhalten:"
htmlFromPlaces $ groupPlaces newPlaces
title :: String
title =
"Mitfahrb\246rse zur ordentlichen Mitgliederversammlung des Bundes der Versicherten, 2014-09-27"
template :: H.Html -> H.Html
template body = H.docTypeHtml $ do
H.head $ H.title $ h title
H.body body
htmlFromBool :: Bool -> H.Html
htmlFromBool b = h $ if b then "x" else ""
csvFromBool :: Bool -> String
csvFromBool b = if b then "x" else ""
csvPath :: FilePath
csvPath = "carpool.csv"
main :: IO ()
main = Happstack.simpleHTTP (Happstack.nullConf{Happstack.port=8080}) site