Split deleteR for use with correctionsR

This commit is contained in:
Gregor Kleen 2018-12-20 12:12:38 +01:00
parent 38dbc0905c
commit 282e0615cb
12 changed files with 191 additions and 62 deletions

3
messages/button/de.msg Normal file
View File

@ -0,0 +1,3 @@
AmbiguousButtons: Mehrere Submit-Buttons aktiv
WrongButtonValue: Submit-Button hat falschen Wert
MultipleButtonValues: Submit-Button hat mehrere Werte

View File

@ -17,6 +17,7 @@ data DummyMessage = MsgDummyIdent
dummyForm :: ( RenderMessage site FormMessage
, RenderMessage site DummyMessage
, RenderMessage site ButtonMessage
, YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site)
, Button site SubmitButton
@ -33,6 +34,7 @@ dummyLogin :: ( YesodAuth site
, SqlBackendCanRead (YesodPersistBackend site)
, RenderMessage site FormMessage
, RenderMessage site DummyMessage
, RenderMessage site ButtonMessage
, Button site SubmitButton
, Show (ButtonCssClass site)
) => AuthPlugin site

View File

@ -53,6 +53,7 @@ userPrincipalName = Ldap.Attr "userPrincipalName"
campusForm :: ( RenderMessage site FormMessage
, RenderMessage site CampusMessage
, RenderMessage site ButtonMessage
, Button site SubmitButton
, Show (ButtonCssClass site)
) => AForm (HandlerT site IO) CampusLogin
@ -65,6 +66,7 @@ campusLogin :: forall site.
( YesodAuth site
, RenderMessage site FormMessage
, RenderMessage site CampusMessage
, RenderMessage site ButtonMessage
, Button site SubmitButton
, Show (ButtonCssClass site)
) => LdapConf -> LdapPool -> AuthPlugin site

View File

@ -27,6 +27,7 @@ data PWHashMessage = MsgPWHashIdent
hashForm :: ( RenderMessage site FormMessage
, RenderMessage site PWHashMessage
, RenderMessage site ButtonMessage
, Button site SubmitButton
, Show (ButtonCssClass site)
) => AForm (HandlerT site IO) HashLogin
@ -41,6 +42,7 @@ hashLogin :: ( YesodAuth site
, SqlBackendCanRead (YesodPersistBackend site)
, RenderMessage site FormMessage
, RenderMessage site PWHashMessage
, RenderMessage site ButtonMessage
, Button site SubmitButton
, Show (ButtonCssClass site)
) => PWHashAlgorithm -> AuthPlugin site

View File

@ -160,6 +160,7 @@ mkMessage "UniWorX" "messages/uniworx" "de"
mkMessageVariant "UniWorX" "Campus" "messages/campus" "de"
mkMessageVariant "UniWorX" "Dummy" "messages/dummy" "de"
mkMessageVariant "UniWorX" "PWHash" "messages/pw-hash" "de"
mkMessageVariant "UniWorX" "Button" "messages/button" "de"
-- This instance is required to use forms. You can modify renderMessage to
-- achieve customized and internationalized form validation messages.

View File

@ -1,6 +1,7 @@
module Handler.Utils.Delete
( DeleteRoute(..)
, deleteR
, postDeleteR, getDeleteR
) where
import Import
@ -13,63 +14,90 @@ import qualified Data.Set as Set
import qualified Data.CaseInsensitive as CI
import Control.Monad.Trans.Random
import System.Random (mkStdGen)
import System.Random.Shuffle (shuffleM)
import qualified Crypto.Hash as Crypto (hash)
import Crypto.Hash (Digest, SHAKE128)
import qualified Data.ByteArray as ByteArray
import Data.Char (isAlphaNum)
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect)
import qualified Database.Esqueleto.Internal.Language as E (From)
data DeleteRoute = forall record. (DeleteCascade record SqlBackend, Hashable (Key record)) => DeleteRoute
data DeleteRoute record = forall tables infoExpr info. (E.SqlSelect infoExpr info, E.From E.SqlQuery E.SqlExpr SqlBackend tables) => DeleteRoute
{ drRecords :: Set (Key record)
, drRenderRecord :: Entity record -> ReaderT SqlBackend (HandlerT UniWorX IO) Widget
, drRecordConfirmString :: Entity record -> ReaderT SqlBackend (HandlerT UniWorX IO) Text
, drUnjoin :: tables -> E.SqlExpr (Entity record)
, drGetInfo :: tables -> E.SqlQuery infoExpr
, drRenderRecord :: info -> ReaderT SqlBackend (HandlerT UniWorX IO) Widget
, drRecordConfirmString :: info -> ReaderT SqlBackend (HandlerT UniWorX IO) Text
, drCaption
, drSuccessMessage :: SomeMessage UniWorX
, drAbort
, drSuccess :: SomeRoute UniWorX
}
confirmForm :: ( MonadHandler m, HandlerSite m ~ UniWorX )
=> Text -- ^ Confirmation string
-> AForm m Bool
confirmForm confirmString = flip traverseAForm aform $ \case
(inpConfirmStr, BtnDelete)
| ((==) `on` map (CI.mk . filter isAlphaNum) . Text.words) confirmString inpConfirmStr
-> return $ pure True
| otherwise
-> formFailure [MsgDeleteConfirmationWrong]
(_, BtnAbort)
-> return $ pure False
where
aform = (,)
<$> areq confirmField (fslI MsgDeleteConfirmation) Nothing
<*> disambiguateButtons (combinedButtonFieldF "")
confirmField
| multiple = convertField unTextarea Textarea textareaField
| otherwise = textField
multiple = length (filter (not . Text.null . Text.strip) $ Text.lines confirmString) > 1
deleteR :: DeleteRoute -> Handler Html
deleteR DeleteRoute{..} = do
targets <- runDB . mconcatForM drRecords $ \rKey -> do
ent <- Entity rKey <$> get404 rKey
recordWdgt <- drRenderRecord ent
recordConfirmString <- drRecordConfirmString ent
return $ pure (recordWdgt, recordConfirmString)
cIDKey <- hash . (ByteArray.convert :: Digest (SHAKE128 64) -> ByteString) . Crypto.hash <$> getsYesod appCryptoIDKey
postDeleteR :: ( DeleteCascade record SqlBackend )
=> (Set (Key record) -> DeleteRoute record) -- ^ Construct `DeleteRoute` based on incoming record keys
-> Handler ()
-- | Perform deletion
postDeleteR mkRoute = do
drResult <- fmap (fmap mkRoute) . runInputPost . iopt secretJsonField $ toPathPiece PostDeleteTarget
let sTargets = evalRand (shuffleM targets) . mkStdGen . hashWithSalt cIDKey $ Set.toList drRecords
confirmString = Text.unlines $ map (Text.strip . view _2) sTargets
confirmField
| Set.size drRecords <= 1 = textField
| otherwise = convertField unTextarea Textarea textareaField
((deleteFormRes, deleteFormWdgt), deleteFormEnctype) <- runFormPost . identForm FIDDelete . renderAForm FormStandard $ (,)
<$> areq confirmField (fslI MsgDeleteConfirmation) Nothing
<*> combinedButtonFieldF ""
void . for drResult $ \DeleteRoute{..} -> do
confirmString <- fmap Text.unlines . runDB $ mapM drRecordConfirmString <=< E.select . E.from $ \t -> drGetInfo t <* E.where_ (drUnjoin t E.^. persistIdField `E.in_` E.valList (Set.toList drRecords))
formResult deleteFormRes $ \case
(_, catMaybes -> [BtnAbort]) ->
redirect drAbort
(inpConfirmStr, catMaybes -> [BtnDelete])
| ((==) `on` map (CI.mk . filter isAlphaNum) . Text.words) confirmString inpConfirmStr
-> do
runDB $ do
forM_ drRecords deleteCascade
addMessageI Success drSuccessMessage
redirect drSuccess
| otherwise
-> addMessageI Error MsgDeleteConfirmationWrong
_other -> return ()
let
addDeleteTargets :: Form a -> Form a
addDeleteTargets form csrf = do
(_, fvTargets) <- mreq secretJsonField ("" & addName (toPathPiece PostDeleteTarget)) (Just drRecords)
over _2 (mappend $ fvInput fvTargets) <$> form csrf
((confirmRes, _), _) <- runFormPost . identForm FIDDelete . addDeleteTargets . renderAForm FormStandard $ confirmForm confirmString
formResult confirmRes $ \case
True -> do
runDB $ do
forM_ drRecords deleteCascade
addMessageI Success drSuccessMessage
redirect drSuccess
False ->
redirect drAbort
getDeleteR :: (DeleteCascade record SqlBackend) => DeleteRoute record -> Handler a
getDeleteR DeleteRoute{..} = do
targets <- runDB $ mapM (\i -> (,) <$> drRenderRecord i <*> drRecordConfirmString i) <=< E.select . E.from $ \t -> drGetInfo t <* E.where_ (drUnjoin t E.^. persistIdField `E.in_` E.valList (Set.toList drRecords))
let confirmString = Text.unlines $ view _2 <$> targets
((_, deleteFormWdgt), deleteFormEnctype) <- runFormPost . identForm FIDDelete . renderAForm FormStandard $ confirmForm confirmString
Just targetRoute <- getCurrentRoute
defaultLayout
$(widgetFile "widgets/delete-confirmation")
sendResponse =<<
defaultLayout $(widgetFile "widgets/delete-confirmation")
deleteR :: (DeleteCascade record SqlBackend) => DeleteRoute record -> Handler Html
deleteR dr = do
postDeleteR $ \drRecords -> dr {drRecords}
getDeleteR dr

View File

@ -471,7 +471,10 @@ dbParamsFormWrap DBParamsForm{..} tableForm frag = do
return . (res,) $ do
btnId <- newIdent
act <- traverse toTextUrl dbParamsFormAction
let submitField = buttonField BtnSubmit
let submitField :: Field Handler SubmitButton
submitField = buttonField BtnSubmit
submitView :: Widget
submitView = fieldView submitField btnId "" mempty (Right BtnSubmit) False
enctype' = bool id (mappend $ fieldEnctype submitField) dbParamsFormAddSubmit enctype
$(widgetFile "table/form-wrap")

View File

@ -542,6 +542,20 @@ lookupGlobalGetParam ident = (>>= fromPathPiece) <$> lookupGetParam (toPathPiece
hasGlobalGetParam :: MonadHandler m => GlobalGetParam -> m Bool
hasGlobalGetParam ident = isJust <$> lookupGetParam (toPathPiece ident)
data GlobalPostParam = PostDeleteTarget
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
instance Universe GlobalPostParam
instance Finite GlobalPostParam
nullaryPathPiece ''GlobalPostParam (camelToPathPiece' 1)
lookupGlobalPostParam :: (MonadHandler m, PathPiece result) => GlobalPostParam -> m (Maybe result)
lookupGlobalPostParam ident = (>>= fromPathPiece) <$> lookupPostParam (toPathPiece ident)
hasGlobalPostParam :: MonadHandler m => GlobalPostParam -> m Bool
hasGlobalPostParam ident = isJust <$> lookupPostParam (toPathPiece ident)
---------------------------------
-- Custom HTTP Request-Headers --
---------------------------------

View File

@ -1,6 +1,6 @@
module Utils.Form where
import ClassyPrelude.Yesod hiding (addMessage)
import ClassyPrelude.Yesod hiding (addMessage, cons, Proxy(..))
import Settings
import qualified Text.Blaze.Internal as Blaze (null)
@ -19,7 +19,7 @@ import Control.Monad.Trans.Maybe (MaybeT(..))
import Data.List ((!!))
import Control.Lens ((&))
import Control.Lens
import Web.PathPieces
@ -27,6 +27,8 @@ import Data.UUID
import Utils.Message
import Data.Proxy
-------------------
-- Form Renderer --
-------------------
@ -36,7 +38,7 @@ data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize
renderAForm :: Monad m => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do
(res, ($ []) -> views) <- aFormToForm aform
(res, ($ []) -> fieldViews) <- aFormToForm aform
let widget = $(widgetFile "widgets/form")
return (res, widget)
@ -206,32 +208,51 @@ class (Enum a, Bounded a, Ord a, PathPiece a) => Button site a where
cssClass :: a -> ButtonCssClass site
data ButtonMessage = MsgAmbiguousButtons
| MsgWrongButtonValue
| MsgMultipleButtonValues
data SubmitButton = BtnSubmit
deriving (Enum, Eq, Ord, Bounded, Read, Show)
instance Universe SubmitButton
instance Finite SubmitButton
instance PathPiece SubmitButton where
toPathPiece = showToPathPiece
fromPathPiece = readFromPathPiece
buttonField :: forall site a. (Button site a, Show (ButtonCssClass site)) => a -> Field (HandlerT site IO) a -- already validates that the correct button press was received (result only neccessary for combinedButtonField)
buttonField :: forall a m.
( Button (HandlerSite m) a
, Show (ButtonCssClass (HandlerSite m))
, RenderMessage (HandlerSite m) ButtonMessage
, Monad m
) => a -> Field m a
-- | Already validates that the correct button press was received (result only neccessary for combinedButtonField)
buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
where
fieldEnctype = UrlEncoded
fieldView :: FieldViewFunc m a
fieldView fid name attrs _val _ = let
cssClass' :: ButtonCssClass site
cssClass' :: ButtonCssClass (HandlerSite m)
cssClass' = cssClass btn
in [whamlet|
<button .btn .#{bcc2txt cssClass'} type=submit name=#{name} value=#{toPathPiece btn} *{attrs} ##{fid}>^{label btn}
|]
fieldParse [] _ = return $ Right Nothing
fieldParse [str] _
fieldParse [] [] = return $ Right Nothing
fieldParse [str] []
| str == toPathPiece btn = return $ Right $ Just btn
| otherwise = return $ Left "Wrong button value"
fieldParse _ _ = return $ Left "Multiple button values"
| otherwise = return . Left $ SomeMessage MsgWrongButtonValue
fieldParse _ _ = return . Left $ SomeMessage MsgMultipleButtonValues
combinedButtonField :: (Button site a, Show (ButtonCssClass site)) => [a] -> FieldSettings site -> AForm (HandlerT site IO) [Maybe a]
combinedButtonField :: forall a m.
( Button (HandlerSite m) a
, Show (ButtonCssClass (HandlerSite m))
, RenderMessage (HandlerSite m) ButtonMessage
, MonadHandler m
) => [a] -> FieldSettings (HandlerSite m) -> AForm m [Maybe a]
combinedButtonField bs FieldSettings{..} = formToAForm $ do
mr <- getMessageRender
fvId <- maybe newIdent return fsId
@ -250,13 +271,45 @@ combinedButtonField bs FieldSettings{..} = formToAForm $ do
}
)
combinedButtonFieldF :: forall site a. (Button site a, Show (ButtonCssClass site), Finite a) => FieldSettings site -> AForm (HandlerT site IO) [Maybe a]
combinedButtonFieldF :: forall m a.
( Button (HandlerSite m) a
, Show (ButtonCssClass (HandlerSite m))
, RenderMessage (HandlerSite m) ButtonMessage
, Finite a
, MonadHandler m
) => FieldSettings (HandlerSite m) -> AForm m [Maybe a]
combinedButtonFieldF = combinedButtonField (universeF :: [a])
submitButton :: (Button site SubmitButton, Show (ButtonCssClass site)) => AForm (HandlerT site IO) ()
submitButton = void $ combinedButtonField [BtnSubmit] ""
disambiguateButtons :: forall m a.
( MonadHandler m
, RenderMessage (HandlerSite m) ButtonMessage
) => AForm m [Maybe a] -> AForm m a
disambiguateButtons = traverseAForm $ \case
(catMaybes -> [bRes]) -> return $ FormSuccess bRes
(catMaybes -> [] ) -> return FormMissing
_other -> formFailure [MsgAmbiguousButtons]
autosubmitButton :: (Button site SubmitButton, Show (ButtonCssClass site)) => AForm (HandlerT site IO) ()
combinedButtonField_ :: forall a m.
( Button (HandlerSite m) a
, Show (ButtonCssClass (HandlerSite m))
, RenderMessage (HandlerSite m) ButtonMessage
, MonadHandler m
) => [a] -> FieldSettings (HandlerSite m) -> AForm m ()
combinedButtonField_ bs fs = void . disambiguateButtons $ combinedButtonField bs fs
combinedButtonFieldF_ :: forall m a p.
( Button (HandlerSite m) a
, Show (ButtonCssClass (HandlerSite m))
, RenderMessage (HandlerSite m) ButtonMessage
, MonadHandler m
, Finite a
) => p a -> FieldSettings (HandlerSite m) -> AForm m ()
combinedButtonFieldF_ _ fs = void . disambiguateButtons $ combinedButtonFieldF @m @a fs
submitButton :: (Button (HandlerSite m) SubmitButton, Show (ButtonCssClass (HandlerSite m)), MonadHandler m, RenderMessage (HandlerSite m) ButtonMessage) => AForm m ()
submitButton = combinedButtonFieldF_ (Proxy @SubmitButton) ""
autosubmitButton :: (Button (HandlerSite m) SubmitButton, Show (ButtonCssClass (HandlerSite m)), MonadHandler m, RenderMessage (HandlerSite m) ButtonMessage) => AForm m ()
autosubmitButton = void $ aopt (buttonField BtnSubmit) ("" & addAutosubmit) Nothing
-------------------
@ -331,6 +384,27 @@ optionsFinite = do
-- Form evaluation --
---------------------
traverseAForm :: forall m a b. Monad m => (a -> m (FormResult b)) -> (AForm m a -> AForm m b)
traverseAForm adj (AForm f) = AForm $ \mr env ints -> do
ret@(res, _, _, _) <- f mr env ints
case res of
FormFailure errs
-> return $ ret & _1 .~ FormFailure errs
FormMissing
-> return $ ret & _1 .~ FormMissing
FormSuccess a -> do
a' <- adj a
return $ ret & _1 .~ a'
formFailure :: forall msg m a.
( MonadHandler m
, RenderMessage (HandlerSite m) msg
) => [msg] -> m (FormResult a)
formFailure errs' = do
mr <- getMessageRender
return . FormFailure $ map mr errs'
formResult :: MonadHandler m => FormResult a -> (a -> m ()) -> m ()
formResult res f = void . formResultMaybe res $ \x -> Nothing <$ f x

View File

@ -2,4 +2,4 @@ $newline never
<form method=#{decodeUtf8 (renderStdMethod dbParamsFormMethod)} action=#{fromMaybe "" act} *{dbParamsFormAttrs} enctype=#{enctype'}>
^{fWidget}
$if dbParamsFormAddSubmit
^{fieldView submitField btnId "" mempty (Right BtnSubmit) False}
^{submitView}

View File

@ -1,6 +1,6 @@
<p>_{drCaption}
<ul>
$forall (wdgt, _) <- sTargets
$forall (wdgt, _) <- targets
<li>
^{wdgt}

View File

@ -2,10 +2,10 @@ $newline never
#{fragment}
$case formLayout
$of FormDBTablePagesize
$forall view <- views
$forall view <- fieldViews
^{fvInput view}
$of _
$forall view <- views
$forall view <- fieldViews
$# TODO: add class 'form-group--submit' if this is the submit-button view
<div .form-group :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error>
$if not (Blaze.null $ fvLabel view)