Beginning of serious refactor

This commit is contained in:
Michael Snoyman 2011-05-09 17:28:42 +03:00
parent 713304e7ef
commit 010cb4863b
4 changed files with 514 additions and 661 deletions

View File

@ -1,444 +1,251 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoMonomorphismRestriction #-} -- FIXME remove
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE CPP #-}
module Yesod.Form.Fields
( -- * Fields
-- ** Required
stringField
( textField
, passwordField
, textareaField
, hiddenField
, intField
, doubleField
, dayField
, timeField
, htmlField
, selectField
, radioField
, boolField
, emailField
, searchField
, AutoFocus
, urlField
, fileField
-- ** Optional
, maybeStringField
, maybePasswordField
, maybeTextareaField
, maybeHiddenField
, maybeIntField
, maybeDoubleField
, maybeDayField
, maybeTimeField
, maybeHtmlField
, maybeSelectField
, maybeRadioField
, maybeEmailField
, maybeSearchField
, maybeUrlField
, maybeFileField
{- FIXME
-- * Inputs
-- ** Required
, stringInput
, intInput
, boolInput
, dayInput
, emailInput
, urlInput
-- ** Optional
, maybeStringInput
, maybeDayInput
, maybeIntInput
-}
, doubleField
, parseDate
, parseTime
, Textarea (..)
) where
import Yesod.Form.Core
import Yesod.Form.Profiles
import Yesod.Request (FileInfo)
import Yesod.Widget (GWidget)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (ask)
import Data.Time (Day, TimeOfDay)
import Text.Hamlet
import Data.Monoid
import Control.Monad (join)
import Data.Maybe (fromMaybe, isNothing)
import Data.Text (Text, unpack)
import qualified Data.Text as T
import Yesod.Form.Types
import Yesod.Widget
import Text.Hamlet hiding (renderHtml)
import Text.Blaze (ToHtml (..))
import Text.Cassius
import Data.Time (Day, TimeOfDay(..))
import qualified Text.Email.Validate as Email
import Network.URI (parseURI)
import Database.Persist (PersistField)
import Text.HTML.SanitizeXSS (sanitizeBalance)
import Control.Monad (when)
import qualified Blaze.ByteString.Builder.Html.Utf8 as B
import Blaze.ByteString.Builder (writeByteString, toLazyByteString)
import Blaze.ByteString.Builder.Internal.Write (fromWriteList)
import Text.Blaze.Renderer.String (renderHtml)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import Data.Text (Text, unpack, pack)
#if __GLASGOW_HASKELL__ >= 700
#define HAMLET hamlet
#define CASSIUS cassius
#define JULIUS julius
#else
#define HAMLET $hamlet
#define CASSIUS $cassius
#define JULIUS $julius
#endif
stringField = requiredFieldHelper stringFieldProfile
maybeStringField = optionalFieldHelper stringFieldProfile
passwordField = requiredFieldHelper passwordFieldProfile
maybePasswordField = optionalFieldHelper passwordFieldProfile
{- FIXME
intInput n =
mapFormXml fieldsToInput $
requiredFieldHelper intFieldProfile (nameSettings n) Nothing
maybeIntInput n =
mapFormXml fieldsToInput $
optionalFieldHelper intFieldProfile (nameSettings n) Nothing
-}
intField = requiredFieldHelper intFieldProfile
maybeIntField = optionalFieldHelper intFieldProfile
doubleField = requiredFieldHelper doubleFieldProfile
maybeDoubleField = optionalFieldHelper doubleFieldProfile
dayField = requiredFieldHelper dayFieldProfile
maybeDayField = optionalFieldHelper dayFieldProfile
timeField = requiredFieldHelper timeFieldProfile
maybeTimeField = optionalFieldHelper timeFieldProfile
boolField ffs orig = do
env <- askParams
let label = ffsLabel ffs
tooltip = ffsTooltip ffs
name <- maybe newFormIdent return $ ffsName ffs
theId <- maybe newFormIdent return $ ffsId ffs
let (res, val) =
if null env
then (FormMissing, fromMaybe False orig)
else case lookup name env of
Nothing -> (FormSuccess False, False)
Just "" -> (FormSuccess False, False)
Just "false" -> (FormSuccess False, False)
Just _ -> (FormSuccess True, True)
let fi = FieldInfo
{ fiLabel = toHtml label
, fiTooltip = tooltip
, fiIdent = theId
, fiInput = [HAMLET|
<input id="#{theId}" type="checkbox" name="#{name}" :val:checked="">
intField :: (Monad monad, Integral i) => Field (GGWidget master monad ()) i
intField = Field
{ fieldParse = maybe (Left "Invalid integer") Right . readMayI . unpack -- FIXME Data.Text.Read
, fieldRender = pack . showI
, fieldView = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="number" :isReq:required="" value="#{val}">
|]
, fiErrors = case res of
FormFailure [x] -> Just $ toHtml x
_ -> Nothing
, fiRequired = True
}
return (res, fi, UrlEncoded)
}
where
showI x = show (fromIntegral x :: Integer)
readMayI s = case reads s of
(x, _):_ -> Just $ fromInteger x
[] -> Nothing
htmlField = requiredFieldHelper htmlFieldProfile
maybeHtmlField = optionalFieldHelper htmlFieldProfile
selectField pairs ffs initial = do
env <- askParams
let label = ffsLabel ffs
tooltip = ffsTooltip ffs
theId <- maybe newFormIdent return $ ffsId ffs
name <- maybe newFormIdent return $ ffsName ffs
let pairs' = zip [1 :: Int ..] pairs
let res = case lookup name env of
Nothing -> FormMissing
Just "none" -> FormFailure ["Field is required"]
Just x ->
case reads $ unpack x of
(x', _):_ ->
case lookup x' pairs' of
Nothing -> FormFailure ["Invalid entry"]
Just (y, _) -> FormSuccess y
[] -> FormFailure ["Invalid entry"]
let isSelected x =
case res of
FormSuccess y -> x == y
_ -> Just x == initial
let input =
#if __GLASGOW_HASKELL__ >= 700
[hamlet|
#else
[$hamlet|
#endif
<select id="#{theId}" name="#{name}">
<option value="none">
$forall pair <- pairs'
<option value="#{show (fst pair)}" :isSelected (fst (snd pair)):selected="">#{snd (snd pair)}
doubleField :: Monad monad => Field (GGWidget master monad ()) Double
doubleField = Field
{ fieldParse = maybe (Left "Invalid number") Right . readMay . unpack -- FIXME use Data.Text.Read
, fieldRender = pack . show
, fieldView = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="text" :isReq:required="" value="#{val}">
|]
let fi = FieldInfo
{ fiLabel = toHtml label
, fiTooltip = tooltip
, fiIdent = theId
, fiInput = input
, fiErrors = case res of
FormFailure [x] -> Just $ toHtml x
_ -> Nothing
, fiRequired = True
}
return (res, fi, UrlEncoded)
}
maybeSelectField pairs ffs initial' = do
env <- askParams
let initial = join initial'
label = ffsLabel ffs
tooltip = ffsTooltip ffs
theId <- maybe newFormIdent return $ ffsId ffs
name <- maybe newFormIdent return $ ffsName ffs
let pairs' = zip [1 :: Int ..] pairs
let res = case lookup name env of
Nothing -> FormMissing
Just "none" -> FormSuccess Nothing
Just x ->
case reads $ unpack x of
(x', _):_ ->
case lookup x' pairs' of
Nothing -> FormFailure ["Invalid entry"]
Just (y, _) -> FormSuccess $ Just y
[] -> FormFailure ["Invalid entry"]
let isSelected x =
case res of
FormSuccess y -> Just x == y
_ -> Just x == initial
let input =
#if __GLASGOW_HASKELL__ >= 700
[hamlet|
#else
[$hamlet|
#endif
<select id="#{theId}" name="#{name}">
<option value="none">
$forall pair <- pairs'
<option value="#{show (fst pair)}" :isSelected (fst (snd pair)):selected="">#{snd (snd pair)}
dayField :: Monad monad => Field (GGWidget master monad ()) Day
dayField = Field
{ fieldParse = parseDate . unpack
, fieldRender = pack . show
, fieldView = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="date" :isReq:required="" value="#{val}">
|]
let fi = FieldInfo
{ fiLabel = toHtml label
, fiTooltip = tooltip
, fiIdent = theId
, fiInput = input
, fiErrors = case res of
FormFailure [x] -> Just $ toHtml x
_ -> Nothing
, fiRequired = False
}
return (res, fi, UrlEncoded)
}
{- FIXME
stringInput :: Text -> FormInput sub master Text
stringInput n =
mapFormXml fieldsToInput $
requiredFieldHelper stringFieldProfile (nameSettings n) Nothing
maybeStringInput :: Text -> FormInput sub master (Maybe Text)
maybeStringInput n =
mapFormXml fieldsToInput $
optionalFieldHelper stringFieldProfile (nameSettings n) Nothing
boolInput :: Text -> FormInput sub master Bool
boolInput n = GForm $ do
env <- askParams
let res = case lookup n env of
Nothing -> FormSuccess False
Just "" -> FormSuccess False
Just "false" -> FormSuccess False
Just _ -> FormSuccess True
let xml = [HAMLET|
<input id="#{n}" type="checkbox" name="#{n}">
timeField :: Monad monad => Field (GGWidget master monad ()) TimeOfDay
timeField = Field
{ fieldParse = parseTime . unpack
, fieldRender = pack . show . roundFullSeconds
, fieldView = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" :isReq:required="" value="#{val}">
|]
return (res, [xml], UrlEncoded)
}
where
roundFullSeconds tod =
TimeOfDay (todHour tod) (todMin tod) fullSec
where
fullSec = fromInteger $ floor $ todSec tod
dayInput :: Text -> FormInput sub master Day
dayInput n =
mapFormXml fieldsToInput $
requiredFieldHelper dayFieldProfile (nameSettings n) Nothing
maybeDayInput :: Text -> FormInput sub master (Maybe Day)
maybeDayInput n =
mapFormXml fieldsToInput $
optionalFieldHelper dayFieldProfile (nameSettings n) Nothing
-}
nameSettings :: Text -> FormFieldSettings
nameSettings n = FormFieldSettings mempty mempty (Just n) (Just n)
urlField = requiredFieldHelper urlFieldProfile
maybeUrlField = optionalFieldHelper urlFieldProfile
{- FIXME
urlInput :: Text -> FormInput sub master Text
urlInput n =
mapFormXml fieldsToInput $
requiredFieldHelper urlFieldProfile (nameSettings n) Nothing
-}
emailField = requiredFieldHelper emailFieldProfile
maybeEmailField = optionalFieldHelper emailFieldProfile
{- FIXME
emailInput :: Text -> FormInput sub master Text
emailInput n =
mapFormXml fieldsToInput $
requiredFieldHelper emailFieldProfile (nameSettings n) Nothing
-}
searchField = requiredFieldHelper . searchFieldProfile
maybeSearchField = optionalFieldHelper . searchFieldProfile
textareaField = requiredFieldHelper textareaFieldProfile
maybeTextareaField = optionalFieldHelper textareaFieldProfile
hiddenField = requiredFieldHelper hiddenFieldProfile
maybeHiddenField = optionalFieldHelper hiddenFieldProfile
fileField ffs = do
env <- lift ask
fenv <- lift $ lift ask
let (FormFieldSettings label tooltip theId' name') = ffs
name <- maybe newFormIdent return name'
theId <- maybe newFormIdent return theId'
let res =
if null env && null fenv
then FormMissing
else case lookup name fenv of
Nothing -> FormFailure ["File is required"]
Just x -> FormSuccess x
let fi = FieldInfo
{ fiLabel = toHtml label
, fiTooltip = tooltip
, fiIdent = theId
, fiInput = fileWidget theId name True
, fiErrors = case res of
FormFailure [x] -> Just $ toHtml x
_ -> Nothing
, fiRequired = True
}
let res' = case res of
FormFailure [e] -> FormFailure [T.concat [label, ": ", e]]
_ -> res
return (res', fi, Multipart)
maybeFileField ffs = do
fenv <- lift $ lift ask
let (FormFieldSettings label tooltip theId' name') = ffs
name <- maybe newFormIdent return name'
theId <- maybe newFormIdent return theId'
let res = FormSuccess $ lookup name fenv
let fi = FieldInfo
{ fiLabel = toHtml label
, fiTooltip = tooltip
, fiIdent = theId
, fiInput = fileWidget theId name False
, fiErrors = Nothing
, fiRequired = True
}
return (res, fi, Multipart)
fileWidget :: Text -> Text -> Bool -> GWidget s m ()
fileWidget theId name isReq = [HAMLET|
<input id="#{theId}" type="file" name="#{name}" :isReq:required="">
htmlField :: Monad monad => Field (GGWidget master monad ()) Html
htmlField = Field
{ fieldParse = Right . preEscapedString . sanitizeBalance . unpack -- FIXME make changes to xss-sanitize
, fieldRender = pack . renderHtml
, fieldView = \theId name val _isReq -> addHamlet
[HAMLET|\
<textarea id="#{theId}" name="#{name}" .html>#{val}
|]
}
radioField pairs ffs initial = do
env <- askParams
let label = ffsLabel ffs
tooltip = ffsTooltip ffs
theId <- maybe newFormIdent return $ ffsId ffs
name <- maybe newFormIdent return $ ffsName ffs
let pairs' = zip [1 :: Int ..] pairs
let res = case lookup name env of
Nothing -> FormMissing
Just "none" -> FormFailure ["Field is required"]
Just x ->
case reads $ unpack x of
(x', _):_ ->
case lookup x' pairs' of
Nothing -> FormFailure ["Invalid entry"]
Just (y, _) -> FormSuccess y
[] -> FormFailure ["Invalid entry"]
let isSelected x =
case res of
FormSuccess y -> x == y
_ -> Just x == initial
let input = [HAMLET|
<div id="#{theId}">
$forall pair <- pairs'
<div>
<input id="#{theId}-#{show (fst pair)}" type="radio" name="#{name}" value="#{show (fst pair)}" :isSelected (fst (snd pair)):checked="">
<label for="#{name}-#{show (fst pair)}">#{snd (snd pair)}
|]
let fi = FieldInfo
{ fiLabel = toHtml label
, fiTooltip = tooltip
, fiIdent = theId
, fiInput = input
, fiErrors = case res of
FormFailure [x] -> Just $ toHtml x
_ -> Nothing
, fiRequired = True
}
return (res, fi, UrlEncoded)
-- | A newtype wrapper around a 'String' that converts newlines to HTML
-- br-tags.
newtype Textarea = Textarea { unTextarea :: Text }
deriving (Show, Read, Eq, PersistField)
instance ToHtml Textarea where
toHtml =
unsafeByteString
. S.concat
. L.toChunks
. toLazyByteString
. fromWriteList writeHtmlEscapedChar
. unpack
. unTextarea
where
-- Taken from blaze-builder and modified with newline handling.
writeHtmlEscapedChar '\n' = writeByteString "<br>"
writeHtmlEscapedChar c = B.writeHtmlEscapedChar c
maybeRadioField pairs ffs initial' = do
env <- askParams
let initial = join initial'
label = ffsLabel ffs
tooltip = ffsTooltip ffs
theId <- maybe newFormIdent return $ ffsId ffs
name <- maybe newFormIdent return $ ffsName ffs
let pairs' = zip [1 :: Int ..] pairs
let res = case lookup name env of
Nothing -> FormMissing
Just "none" -> FormSuccess Nothing
Just x ->
case reads $ unpack x of
(x', _):_ ->
case lookup x' pairs' of
Nothing -> FormFailure ["Invalid entry"]
Just (y, _) -> FormSuccess $ Just y
[] -> FormFailure ["Invalid entry"]
let isSelected x =
case res of
FormSuccess y -> Just x == y
_ -> Just x == initial
let isNone =
case res of
FormSuccess Nothing -> True
FormSuccess Just{} -> False
_ -> isNothing initial
let input =
#if __GLASGOW_HASKELL__ >= 700
[hamlet|
#else
[$hamlet|
#endif
<div id="#{theId}">
$forall pair <- pairs'
<div>
<input id="#{theId}-none" type="radio" name="#{name}" value="none" :isNone:checked="">None
<div>
<input id="#{theId}-#{show (fst pair)}" type="radio" name="#{name}" value="#{show (fst pair)}" :isSelected (fst (snd pair)):checked="">
<label for="#{name}-#{show (fst pair)}">#{snd (snd pair)}
textareaField :: Monad monad => Field (GGWidget master monad ()) Textarea
textareaField = Field
{ fieldParse = Right . Textarea
, fieldRender = unTextarea
, fieldView = \theId name val _isReq -> addHamlet
[HAMLET|\
<textarea id="#{theId}" name="#{name}">#{val}
|]
let fi = FieldInfo
{ fiLabel = toHtml label
, fiTooltip = tooltip
, fiIdent = theId
, fiInput = input
, fiErrors = case res of
FormFailure [x] -> Just $ toHtml x
_ -> Nothing
, fiRequired = False
}
return (res, fi, UrlEncoded)
}
hiddenField :: Monad monad => Field (GGWidget master monad ()) Text
hiddenField = Field
{ fieldParse = Right
, fieldRender = id
, fieldView = \theId name val _isReq -> addHamlet
[HAMLET|\
<input type="hidden" id="#{theId}" name="#{name}" value="#{val}">
|]
}
textField :: Monad monad => Field (GGWidget master monad ()) Text
textField = Field
{ fieldParse = Right
, fieldRender = id
, fieldView = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="text" :isReq:required="" value="#{val}">
|]
}
passwordField :: Monad monad => Field (GGWidget master monad ()) Text
passwordField = Field
{ fieldParse = Right
, fieldRender = id
, fieldView = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="password" :isReq:required="" value="#{val}">
|]
}
readMay :: Read a => String -> Maybe a
readMay s = case reads s of
(x, _):_ -> Just x
[] -> Nothing
parseDate :: String -> Either Text Day
parseDate = maybe (Left "Invalid day, must be in YYYY-MM-DD format") Right
. readMay . replace '/' '-'
-- | Replaces all instances of a value in a list by another value.
-- from http://hackage.haskell.org/packages/archive/cgi/3001.1.7.1/doc/html/src/Network-CGI-Protocol.html#replace
replace :: Eq a => a -> a -> [a] -> [a]
replace x y = map (\z -> if z == x then y else z)
parseTime :: String -> Either Text TimeOfDay
parseTime (h2:':':m1:m2:[]) = parseTimeHelper ('0', h2, m1, m2, '0', '0')
parseTime (h1:h2:':':m1:m2:[]) = parseTimeHelper (h1, h2, m1, m2, '0', '0')
parseTime (h1:h2:':':m1:m2:' ':'A':'M':[]) =
parseTimeHelper (h1, h2, m1, m2, '0', '0')
parseTime (h1:h2:':':m1:m2:' ':'P':'M':[]) =
let [h1', h2'] = show $ (read [h1, h2] :: Int) + 12
in parseTimeHelper (h1', h2', m1, m2, '0', '0')
parseTime (h1:h2:':':m1:m2:':':s1:s2:[]) =
parseTimeHelper (h1, h2, m1, m2, s1, s2)
parseTime _ = Left "Invalid time, must be in HH:MM[:SS] format"
parseTimeHelper :: (Char, Char, Char, Char, Char, Char)
-> Either Text TimeOfDay
parseTimeHelper (h1, h2, m1, m2, s1, s2)
| h < 0 || h > 23 = Left $ pack $ "Invalid hour: " ++ show h
| m < 0 || m > 59 = Left $ pack $ "Invalid minute: " ++ show m
| s < 0 || s > 59 = Left $ pack $ "Invalid second: " ++ show s
| otherwise = Right $ TimeOfDay h m s
where
h = read [h1, h2]
m = read [m1, m2]
s = fromInteger $ read [s1, s2]
emailField :: Monad monad => Field (GGWidget master monad ()) Text
emailField = Field
{ fieldParse = \s -> if Email.isValid (unpack s)
then Right s
else Left "Invalid e-mail address"
, fieldRender = id
, fieldView = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="email" :isReq:required="" value="#{val}">
|]
}
type AutoFocus = Bool
searchField :: Monad monad => AutoFocus -> Field (GGWidget master monad ()) Text
searchField autoFocus = Field
{ fieldParse = Right
, fieldRender = id
, fieldView = \theId name val isReq -> do
addHtml [HAMLET|\
<input id="#{theId}" name="#{name}" type="search" :isReq:required="" :autoFocus:autofocus="" value="#{val}">
|]
when autoFocus $ do
addHtml $ [HAMLET|\<script>if (!('autofocus' in document.createElement('input'))) {document.getElementById('#{theId}').focus();}</script>
|]
addCassius [CASSIUS|
#{theId}
-webkit-appearance: textfield
|]
}
urlField :: Monad monad => Field (GGWidget master monad ()) Text
urlField = Field
{ fieldParse = \s -> case parseURI $ unpack s of
Nothing -> Left "Invalid URL"
Just _ -> Right s
, fieldRender = id
, fieldView = \theId name val isReq -> addHtml
[HAMLET|
<input ##{theId} name=#{name} type=url :isReq:required value=#{val}>
|]
}

174
Yesod/Form/Functions.hs Normal file
View File

@ -0,0 +1,174 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
module Yesod.Form.Functions
( -- * Running in Form monad
newFormIdent
, askParams
, askFiles
-- * Applicative/Monadic conversion
, formToAForm
, aFormToForm
-- * Fields to Forms
, mreq
, mopt
, areq
, aopt
-- * Run a form
, runFormPost
, runFormPostNoNonce
, runFormGet
-- * Rendering
, FormRender
, renderTable
, renderDivs
) where
import Yesod.Form.Types
import Data.Text (Text, pack)
import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST)
import Control.Monad.Trans.Class (lift)
import Control.Monad (liftM)
import Text.Blaze (Html)
import Yesod.Handler (GHandler, GGHandler, getRequest, runRequestBody)
import Yesod.Widget (GGWidget, whamlet)
import Yesod.Request (reqNonce, reqWaiRequest, reqGetParams)
import Network.Wai (requestMethod)
import Text.Hamlet.NonPoly (html)
import Data.Monoid (mempty)
#if __GLASGOW_HASKELL__ >= 700
#define WHAMLET whamlet
#define HTML html
#else
#define HTML $html
#define WHAMLET $whamlet
#endif
-- | Get a unique identifier.
newFormIdent :: Monad m => Form m Text
newFormIdent = do
i <- get
let i' = incrInts i
put i'
return $ pack $ 'f' : show i'
where
incrInts (IntSingle i) = IntSingle $ i + 1
incrInts (IntCons i is) = (i + 1) `IntCons` is
formToAForm :: Monad m => Form m (FormResult a, xml) -> AForm ([xml] -> [xml]) m a
formToAForm form = AForm $ \env ints -> do
((a, xml), ints', enc) <- runRWST form env ints
return (a, (:) xml, ints', enc)
aFormToForm :: Monad m => AForm xml m a -> Form m (FormResult a, xml)
aFormToForm (AForm aform) = do
ints <- get
env <- ask
(a, xml, ints', enc) <- lift $ aform env ints
put ints'
tell enc
return (a, xml)
askParams :: Monad m => Form m (Maybe Env)
askParams = liftM (liftM fst) ask
askFiles :: Monad m => Form m (Maybe FileEnv)
askFiles = liftM (liftM snd) ask
mreq :: Monad m => Field xml a -> Maybe a -> Form m (FormResult a, xml)
mreq = undefined
mopt :: Monad m => Field xml a -> Maybe (Maybe a) -> Form m (FormResult (Maybe a), xml)
mopt = undefined
areq :: Monad m => Field xml a -> Maybe a -> AForm ([xml] -> [xml]) m a
areq a b = formToAForm $ mreq a b
aopt :: Monad m => Field xml a -> Maybe (Maybe a) -> AForm ([xml] -> [xml]) m (Maybe a)
aopt a b = formToAForm $ mopt a b
runFormGeneric :: Monad m => Form m a -> Maybe (Env, FileEnv) -> m (a, Enctype)
runFormGeneric form env = evalRWST form env (IntSingle 1)
runFormPost :: (Html -> Form (GHandler sub master) (FormResult a, xml)) -> GHandler sub master ((FormResult a, xml), Enctype)
runFormPost form = do
req <- getRequest
let nonceKey = "_nonce"
let nonce =
case reqNonce req of
Nothing -> mempty
Just n -> [HTML|<input type=hidden name=#{nonceKey} value=#{n}>|]
env <- if requestMethod (reqWaiRequest req) == "GET"
then return Nothing
else fmap Just runRequestBody
((res, xml), enctype) <- runFormGeneric (form nonce) env
let res' =
case (res, env) of
(FormSuccess{}, Just (params, _))
| lookup nonceKey params /= reqNonce req ->
FormFailure [csrfWarning]
_ -> res
return ((res', xml), enctype)
csrfWarning :: Text
csrfWarning = "As a protection against cross-site request forgery attacks, please confirm your form submission." -- TRANS
runFormPostNoNonce :: (Html -> Form (GHandler sub master) a) -> GHandler sub master (a, Enctype)
runFormPostNoNonce form = do
req <- getRequest
env <- if requestMethod (reqWaiRequest req) == "GET"
then return Nothing
else fmap Just runRequestBody
runFormGeneric (form mempty) env
runFormGet :: Monad m => (Html -> Form (GGHandler sub master m) a) -> GGHandler sub master m (a, Enctype)
runFormGet form = do
let key = "_hasdata"
let fragment = [HTML|<input type=hidden name=#{key}>|]
gets <- liftM reqGetParams getRequest
let env =
case lookup key gets of
Nothing -> Nothing
Just _ -> Just (gets, [])
runFormGeneric (form fragment) env
type FormRender master m a =
AForm ([FieldView (GGWidget master m ())] -> [FieldView (GGWidget master m ())]) m a
-> Html
-> Form m (FormResult a, GGWidget master m ())
renderTable, renderDivs :: Monad m => FormRender master m a
renderTable aform fragment = do
(res, views') <- aFormToForm aform
let views = views' []
-- FIXME non-valid HTML
let widget = [WHAMLET|
\#{fragment}
$forall view <- views
<tr :fvRequired view:.required :not $ fvRequired view:.optional>
<td>
<label for=#{fvId view}>#{fvLabel view}
$maybe tt <- fvTooltip view
<div .tooltip>#{tt}
<td>^{fvInput view}
$maybe err <- fvErrors view
<td .errors>#{err}
|]
return (res, widget)
renderDivs aform fragment = do
(res, views') <- aFormToForm aform
let views = views' []
let widget = [WHAMLET|
\#{fragment}
$forall view <- views
<div :fvRequired view:.required :not $ fvRequired view:.optional>
<label for=#{fvId view}>#{fvLabel view}
$maybe tt <- fvTooltip view
<div .tooltip>#{tt}
^{fvInput view}
$maybe err <- fvErrors view
<div .errors>#{err}
|]
return (res, widget)

View File

@ -1,251 +0,0 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE CPP #-}
module Yesod.Form.Profiles
( stringFieldProfile
, passwordFieldProfile
, textareaFieldProfile
, hiddenFieldProfile
, intFieldProfile
, dayFieldProfile
, timeFieldProfile
, htmlFieldProfile
, emailFieldProfile
, searchFieldProfile
, AutoFocus
, urlFieldProfile
, doubleFieldProfile
, parseDate
, parseTime
, Textarea (..)
) where
import Yesod.Form.Core
import Yesod.Widget
import Text.Hamlet hiding (renderHtml)
import Text.Blaze (ToHtml (..))
import Text.Cassius
import Data.Time (Day, TimeOfDay(..))
import qualified Text.Email.Validate as Email
import Network.URI (parseURI)
import Database.Persist (PersistField)
import Text.HTML.SanitizeXSS (sanitizeBalance)
import Control.Monad (when)
import qualified Blaze.ByteString.Builder.Html.Utf8 as B
import Blaze.ByteString.Builder (writeByteString, toLazyByteString)
import Blaze.ByteString.Builder.Internal.Write (fromWriteList)
import Text.Blaze.Renderer.String (renderHtml)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import Data.Text (Text, unpack, pack)
#if __GLASGOW_HASKELL__ >= 700
#define HAMLET hamlet
#define CASSIUS cassius
#define JULIUS julius
#else
#define HAMLET $hamlet
#define CASSIUS $cassius
#define JULIUS $julius
#endif
intFieldProfile :: (Monad monad, Integral i) => FieldProfile (GGWidget master monad ()) i
intFieldProfile = FieldProfile
{ fpParse = maybe (Left "Invalid integer") Right . readMayI . unpack -- FIXME Data.Text.Read
, fpRender = pack . showI
, fpWidget = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="number" :isReq:required="" value="#{val}">
|]
}
where
showI x = show (fromIntegral x :: Integer)
readMayI s = case reads s of
(x, _):_ -> Just $ fromInteger x
[] -> Nothing
doubleFieldProfile :: Monad monad => FieldProfile (GGWidget master monad ()) Double
doubleFieldProfile = FieldProfile
{ fpParse = maybe (Left "Invalid number") Right . readMay . unpack -- FIXME use Data.Text.Read
, fpRender = pack . show
, fpWidget = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="text" :isReq:required="" value="#{val}">
|]
}
dayFieldProfile :: Monad monad => FieldProfile (GGWidget master monad ()) Day
dayFieldProfile = FieldProfile
{ fpParse = parseDate . unpack
, fpRender = pack . show
, fpWidget = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="date" :isReq:required="" value="#{val}">
|]
}
timeFieldProfile :: Monad monad => FieldProfile (GGWidget master monad ()) TimeOfDay
timeFieldProfile = FieldProfile
{ fpParse = parseTime . unpack
, fpRender = pack . show . roundFullSeconds
, fpWidget = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" :isReq:required="" value="#{val}">
|]
}
where
roundFullSeconds tod =
TimeOfDay (todHour tod) (todMin tod) fullSec
where
fullSec = fromInteger $ floor $ todSec tod
htmlFieldProfile :: Monad monad => FieldProfile (GGWidget master monad ()) Html
htmlFieldProfile = FieldProfile
{ fpParse = Right . preEscapedString . sanitizeBalance . unpack -- FIXME make changes to xss-sanitize
, fpRender = pack . renderHtml
, fpWidget = \theId name val _isReq -> addHamlet
[HAMLET|\
<textarea id="#{theId}" name="#{name}" .html>#{val}
|]
}
-- | A newtype wrapper around a 'String' that converts newlines to HTML
-- br-tags.
newtype Textarea = Textarea { unTextarea :: Text }
deriving (Show, Read, Eq, PersistField)
instance ToHtml Textarea where
toHtml =
unsafeByteString
. S.concat
. L.toChunks
. toLazyByteString
. fromWriteList writeHtmlEscapedChar
. unpack
. unTextarea
where
-- Taken from blaze-builder and modified with newline handling.
writeHtmlEscapedChar '\n' = writeByteString "<br>"
writeHtmlEscapedChar c = B.writeHtmlEscapedChar c
textareaFieldProfile :: Monad monad => FieldProfile (GGWidget master monad ()) Textarea
textareaFieldProfile = FieldProfile
{ fpParse = Right . Textarea
, fpRender = unTextarea
, fpWidget = \theId name val _isReq -> addHamlet
[HAMLET|\
<textarea id="#{theId}" name="#{name}">#{val}
|]
}
hiddenFieldProfile :: Monad monad => FieldProfile (GGWidget master monad ()) Text
hiddenFieldProfile = FieldProfile
{ fpParse = Right
, fpRender = id
, fpWidget = \theId name val _isReq -> addHamlet
[HAMLET|\
<input type="hidden" id="#{theId}" name="#{name}" value="#{val}">
|]
}
stringFieldProfile :: Monad monad => FieldProfile (GGWidget master monad ()) Text
stringFieldProfile = FieldProfile
{ fpParse = Right
, fpRender = id
, fpWidget = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="text" :isReq:required="" value="#{val}">
|]
}
passwordFieldProfile :: Monad monad => FieldProfile (GGWidget master monad ()) Text
passwordFieldProfile = FieldProfile
{ fpParse = Right
, fpRender = id
, fpWidget = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="password" :isReq:required="" value="#{val}">
|]
}
readMay :: Read a => String -> Maybe a
readMay s = case reads s of
(x, _):_ -> Just x
[] -> Nothing
parseDate :: String -> Either Text Day
parseDate = maybe (Left "Invalid day, must be in YYYY-MM-DD format") Right
. readMay . replace '/' '-'
-- | Replaces all instances of a value in a list by another value.
-- from http://hackage.haskell.org/packages/archive/cgi/3001.1.7.1/doc/html/src/Network-CGI-Protocol.html#replace
replace :: Eq a => a -> a -> [a] -> [a]
replace x y = map (\z -> if z == x then y else z)
parseTime :: String -> Either Text TimeOfDay
parseTime (h2:':':m1:m2:[]) = parseTimeHelper ('0', h2, m1, m2, '0', '0')
parseTime (h1:h2:':':m1:m2:[]) = parseTimeHelper (h1, h2, m1, m2, '0', '0')
parseTime (h1:h2:':':m1:m2:' ':'A':'M':[]) =
parseTimeHelper (h1, h2, m1, m2, '0', '0')
parseTime (h1:h2:':':m1:m2:' ':'P':'M':[]) =
let [h1', h2'] = show $ (read [h1, h2] :: Int) + 12
in parseTimeHelper (h1', h2', m1, m2, '0', '0')
parseTime (h1:h2:':':m1:m2:':':s1:s2:[]) =
parseTimeHelper (h1, h2, m1, m2, s1, s2)
parseTime _ = Left "Invalid time, must be in HH:MM[:SS] format"
parseTimeHelper :: (Char, Char, Char, Char, Char, Char)
-> Either Text TimeOfDay
parseTimeHelper (h1, h2, m1, m2, s1, s2)
| h < 0 || h > 23 = Left $ pack $ "Invalid hour: " ++ show h
| m < 0 || m > 59 = Left $ pack $ "Invalid minute: " ++ show m
| s < 0 || s > 59 = Left $ pack $ "Invalid second: " ++ show s
| otherwise = Right $ TimeOfDay h m s
where
h = read [h1, h2]
m = read [m1, m2]
s = fromInteger $ read [s1, s2]
emailFieldProfile :: Monad monad => FieldProfile (GGWidget master monad ()) Text
emailFieldProfile = FieldProfile
{ fpParse = \s -> if Email.isValid (unpack s)
then Right s
else Left "Invalid e-mail address"
, fpRender = id
, fpWidget = \theId name val isReq -> addHamlet
[HAMLET|\
<input id="#{theId}" name="#{name}" type="email" :isReq:required="" value="#{val}">
|]
}
type AutoFocus = Bool
searchFieldProfile :: Monad monad => AutoFocus -> FieldProfile (GGWidget master monad ()) Text
searchFieldProfile autoFocus = FieldProfile
{ fpParse = Right
, fpRender = id
, fpWidget = \theId name val isReq -> do
addHtml [HAMLET|\
<input id="#{theId}" name="#{name}" type="search" :isReq:required="" :autoFocus:autofocus="" value="#{val}">
|]
when autoFocus $ do
addHtml $ [HAMLET|\<script>if (!('autofocus' in document.createElement('input'))) {document.getElementById('#{theId}').focus();}</script>
|]
addCassius [CASSIUS|
#{theId}
-webkit-appearance: textfield
|]
}
urlFieldProfile :: Monad monad => FieldProfile (GGWidget master monad ()) Text
urlFieldProfile = FieldProfile
{ fpParse = \s -> case parseURI $ unpack s of
Nothing -> Left "Invalid URL"
Just _ -> Right s
, fpRender = id
, fpWidget = \theId name val isReq -> addHtml
[HAMLET|
<input ##{theId} name=#{name} type=url :isReq:required value=#{val}>
|]
}

123
Yesod/Form/Types.hs Normal file
View File

@ -0,0 +1,123 @@
{-# LANGUAGE OverloadedStrings #-}
module Yesod.Form.Types
( -- * Helpers
Enctype (..)
, FormResult (..)
, Env
, FileEnv
, Ints (..)
-- * Form
, Form
, AForm (..)
-- * Build forms
, Field (..)
, FieldSettings (..)
, FieldView (..)
) where
import Control.Monad.Trans.RWS (RWST)
import Yesod.Request (FileInfo)
import Data.Text (Text)
import Data.Monoid (Monoid (..))
import Text.Blaze (Html, ToHtml (toHtml))
import Control.Applicative ((<$>), Applicative (..))
import Control.Monad (liftM)
import Data.String (IsString (..))
import Control.Monad.Trans.Class (MonadTrans (..))
-- | A form can produce three different results: there was no data available,
-- the data was invalid, or there was a successful parse.
--
-- The 'Applicative' instance will concatenate the failure messages in two
-- 'FormResult's.
data FormResult a = FormMissing
| FormFailure [Text]
| FormSuccess a
deriving Show
instance Functor FormResult where
fmap _ FormMissing = FormMissing
fmap _ (FormFailure errs) = FormFailure errs
fmap f (FormSuccess a) = FormSuccess $ f a
instance Applicative FormResult where
pure = FormSuccess
(FormSuccess f) <*> (FormSuccess g) = FormSuccess $ f g
(FormFailure x) <*> (FormFailure y) = FormFailure $ x ++ y
(FormFailure x) <*> _ = FormFailure x
_ <*> (FormFailure y) = FormFailure y
_ <*> _ = FormMissing
instance Monoid m => Monoid (FormResult m) where
mempty = pure mempty
mappend x y = mappend <$> x <*> y
-- | The encoding type required by a form. The 'ToHtml' instance produces values
-- that can be inserted directly into HTML.
data Enctype = UrlEncoded | Multipart
deriving (Eq, Enum, Bounded)
instance ToHtml Enctype where
toHtml UrlEncoded = "application/x-www-form-urlencoded"
toHtml Multipart = "multipart/form-data"
instance Monoid Enctype where
mempty = UrlEncoded
mappend UrlEncoded UrlEncoded = UrlEncoded
mappend _ _ = Multipart
data Ints = IntCons Int Ints | IntSingle Int
instance Show Ints where
show (IntSingle i) = show i
show (IntCons i is) = show i ++ ('-' : show is)
type Env = [(Text, Text)]
type FileEnv = [(Text, FileInfo)]
type Form m a = RWST (Maybe (Env, FileEnv)) Enctype Ints m a
newtype AForm xml m a = AForm
{ unAForm :: Maybe (Env, FileEnv) -> Ints -> m (FormResult a, xml, Ints, Enctype)
}
instance Monad m => Functor (AForm xml m) where
fmap f (AForm a) =
AForm $ \x y -> liftM go $ a x y
where
go (w, x, y, z) = (fmap f w, x, y, z)
instance (Monad m, Monoid xml) => Applicative (AForm xml m) where
pure x = AForm $ const $ \ints -> return (FormSuccess x, mempty, ints, mempty)
(AForm f) <*> (AForm g) = AForm $ \env ints -> do
(a, b, ints', c) <- f env ints
(x, y, ints'', z) <- g env ints'
return (a <*> x, b `mappend` y, ints'', c `mappend` z)
instance (Monad m, Monoid xml, Monoid a) => Monoid (AForm xml m a) where
mempty = pure mempty
mappend a b = mappend <$> a <*> b
instance Monoid xml => MonadTrans (AForm xml) where
lift mx = AForm $ const $ \ints -> do
x <- mx
return (pure x, mempty, ints, mempty)
data FieldSettings = FieldSettings
{ fsLabel :: Html -- FIXME do we need Text?
, fsTooltip :: Maybe Html
, fsId :: Maybe Text
, fsName :: Maybe Text
}
instance IsString FieldSettings where
fromString s = FieldSettings (fromString s) Nothing Nothing Nothing
data FieldView xml = FieldView
{ fvLabel :: Html
, fvTooltip :: Maybe Html
, fvId :: Text
, fvInput :: xml
, fvErrors :: Maybe Html
, fvRequired :: Bool
}
data Field xml a = Field
{ fieldParse :: Text -> Either Text a -- FIXME probably want to make this more sophisticated, handle no form, no field
, fieldRender :: a -> Text
, fieldView :: Text -- ^ ID
-> Text -- ^ name
-> Text -- ^ value
-> Bool -- ^ required?
-> xml
}