Generic and "safe" deletion widget

This commit is contained in:
Gregor Kleen 2018-12-19 21:55:42 +01:00
parent 45182e5074
commit c6b7ad0580
12 changed files with 207 additions and 38 deletions

View File

@ -74,6 +74,8 @@ CourseRegisterToTip: Anmeldung darf auch ohne Begrenzung möglich sein
CourseDeregisterUntilTip: Abmeldung darf auch ohne Begrenzung möglich sein CourseDeregisterUntilTip: Abmeldung darf auch ohne Begrenzung möglich sein
CourseFilterSearch: Volltext-Suche CourseFilterSearch: Volltext-Suche
CourseFilterRegistered: Registriert CourseFilterRegistered: Registriert
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
CourseDeleted: Kurs gelöscht
NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht. NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht.
NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht. NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht.
@ -89,10 +91,12 @@ SheetTitleNew tid@TermId ssh@SchoolId csh@CourseShorthand
SheetEditHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh} #{sheetName} editieren SheetEditHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh} #{sheetName} editieren
SheetEditOk tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Übungsblatt #{sheetName} aus Kurs #{display tid}-#{display ssh}-#{csh} wurde gespeichert. SheetEditOk tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Übungsblatt #{sheetName} aus Kurs #{display tid}-#{display ssh}-#{csh} wurde gespeichert.
SheetNameDup tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Es gibt bereits ein Übungsblatt #{sheetName} in diesem Kurs #{display tid}-#{display ssh}-#{csh}. SheetNameDup tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Es gibt bereits ein Übungsblatt #{sheetName} in diesem Kurs #{display tid}-#{display ssh}-#{csh}.
SheetDelHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{sheetName} wirklich aus Kurs #{display tid}-#{display ssh}-#{csh} herauslöschen? SheetDelHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{sheetName} wirklich aus Kurs #{display tid}-#{display ssh}-#{csh} herauslöschen? Alle assoziierten Abgaben und Korrekturen gehen ebenfalls verloren!
SheetDelText submissionNo@Int: Dies kann nicht mehr rückgängig gemacht werden! Alle Einreichungen gehen ebenfalls verloren! Es gibt #{display submissionNo} Abgaben.
SheetDelOk tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh}: #{sheetName} gelöscht. SheetDelOk tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh}: #{sheetName} gelöscht.
SheetDeleteQuestion: Wollen Sie das unten aufgeführte Übungsblatt und alle zugehörigen Abgaben wirklich löschen?
SheetDeleted: Übungsblatt gelöscht
SheetUploadMode: Abgabe von Dateien SheetUploadMode: Abgabe von Dateien
SheetSubmissionMode: Abgabe-Modus SheetSubmissionMode: Abgabe-Modus
SheetExercise: Aufgabenstellung SheetExercise: Aufgabenstellung
@ -545,17 +549,19 @@ MenuCorrections: Abgaben
MenuSheetNew: Neues Übungsblatt anlegen MenuSheetNew: Neues Übungsblatt anlegen
MenuCourseEdit: Kurs editieren MenuCourseEdit: Kurs editieren
MenuCourseNewTemplate: Als neuen Kurs klonen MenuCourseNewTemplate: Als neuen Kurs klonen
MenuCourseDelete: Kurs löschen
MenuSubmissionNew: Abgabe anlegen MenuSubmissionNew: Abgabe anlegen
MenuSubmissionOwn: Abgabe MenuSubmissionOwn: Abgabe
MenuCorrectors: Korrektoren MenuCorrectors: Korrektoren
MenuSheetEdit: Übungsblatt editieren MenuSheetEdit: Übungsblatt editieren
MenuSheetDelete: Übungsblatt löschen
MenuCorrectionsUpload: Korrekturen hochladen MenuCorrectionsUpload: Korrekturen hochladen
MenuCorrectionsCreate: Abgaben registrieren MenuCorrectionsCreate: Abgaben registrieren
MenuCorrectionsGrade: Abgaben bewerten MenuCorrectionsGrade: Abgaben bewerten
AuthPredsActive: Aktive Authorisierungsprädikate AuthPredsActive: Aktive Authorisierungsprädikate
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
AuthTagFree: Seite ist generell zugänglich AuthTagFree: Seite ist universell zugänglich
AuthTagAdmin: Nutzer ist Administrator AuthTagAdmin: Nutzer ist Administrator
AuthTagNoEscalation: Nutzer-Rechte werden nicht erweitert AuthTagNoEscalation: Nutzer-Rechte werden nicht erweitert
AuthTagDeprecated: Seite ist nicht überholt AuthTagDeprecated: Seite ist nicht überholt
@ -573,4 +579,8 @@ AuthTagUserSubmissions: Abgaben erfolgen durch Kursteilnehmer
AuthTagCorrectorSubmissions: Abgaben erfolgen durch Korrektoren AuthTagCorrectorSubmissions: Abgaben erfolgen durch Korrektoren
AuthTagAuthentication: Authentifizierung erfüllt Anforderungen AuthTagAuthentication: Authentifizierung erfüllt Anforderungen
AuthTagRead: Zugriff ist nur lesend AuthTagRead: Zugriff ist nur lesend
AuthTagWrite: Zugriff ist i.A. schreibend AuthTagWrite: Zugriff ist i.A. schreibend
DeleteCopyStringIfSure count@Int: Wenn Sie sich sicher sind, dass Sie #{pluralDE count "das obige Objekt" "obige Objekte"} unwiderbringlich löschen möchten, schreiben Sie bitte zunächst den angezeigten Text ab.
DeleteConfirmation: Bestätigung
DeleteConfirmationWrong: Bestätigung muss genau dem angezeigten Text entsprechen.

View File

@ -13,6 +13,7 @@ import qualified Data.CaseInsensitive as CI
import Database.Persist.Sql import Database.Persist.Sql
import Text.Blaze (ToMarkup(..)) import Text.Blaze (ToMarkup(..))
import Text.Shakespeare.Text (ToText(..))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
@ -63,6 +64,9 @@ instance ToMarkup a => ToMarkup (CI a) where
toMarkup = toMarkup . CI.original toMarkup = toMarkup . CI.original
preEscapedToMarkup = preEscapedToMarkup . CI.original preEscapedToMarkup = preEscapedToMarkup . CI.original
instance ToText a => ToText (CI a) where
toText = toText . CI.original
instance ToWidget site a => ToWidget site (CI a) where instance ToWidget site a => ToWidget site (CI a) where
toWidget = toWidget . CI.original toWidget = toWidget . CI.original

View File

@ -146,6 +146,15 @@ pattern CSubmissionR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Cr
pattern CSubmissionR tid ssh csh shn cid ptn pattern CSubmissionR tid ssh csh shn cid ptn
= CSheetR tid ssh csh shn (SubmissionR cid ptn) = CSheetR tid ssh csh shn (SubmissionR cid ptn)
pluralDE :: Int -- ^ Count
-> Text -- ^ Singular
-> Text -- ^ Plural
-> Text
pluralDE num singularForm pluralForm
| num == 1 = singularForm
| otherwise = pluralForm
-- Messages creates type UniWorXMessage and RenderMessage UniWorX instance -- Messages creates type UniWorXMessage and RenderMessage UniWorX instance
mkMessage "UniWorX" "messages/uniworx" "de" mkMessage "UniWorX" "messages/uniworx" "de"
mkMessageVariant "UniWorX" "Campus" "messages/campus" "de" mkMessageVariant "UniWorX" "Campus" "messages/campus" "de"
@ -1197,6 +1206,14 @@ pageActions (CourseR tid ssh csh CShowR) =
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
, MenuItem
{ menuItemType = PageActionSecondary
, menuItemLabel = MsgMenuCourseDelete
, menuItemIcon = Nothing
, menuItemRoute = SomeRoute $ CourseR tid ssh csh CDeleteR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
] ]
pageActions (CourseR tid ssh csh SheetListR) = pageActions (CourseR tid ssh csh SheetListR) =
[ MenuItem [ MenuItem
@ -1257,6 +1274,14 @@ pageActions (CSheetR tid ssh csh shn SShowR) =
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
, MenuItem
{ menuItemType = PageActionSecondary
, menuItemLabel = MsgMenuSheetDelete
, menuItemIcon = Nothing
, menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SDelR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
] ]
pageActions (CSheetR tid ssh csh shn SSubsR) = pageActions (CSheetR tid ssh csh shn SSubsR) =
[ MenuItem [ MenuItem

View File

@ -6,6 +6,7 @@ import Utils.Lens
-- import Utils.DB -- import Utils.DB
import Handler.Utils import Handler.Utils
import Handler.Utils.Table.Cells import Handler.Utils.Table.Cells
import Handler.Utils.Delete
-- import Data.Time -- import Data.Time
import qualified Data.Text as T import qualified Data.Text as T
@ -386,10 +387,24 @@ pgCEditR isGetReq tid ssh csh = do
courseEditHandler isGetReq $ courseToForm <$> course courseEditHandler isGetReq $ courseToForm <$> course
getCDeleteR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCDeleteR, postCDeleteR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCDeleteR = error "TODO: implement getCDeleteR" getCDeleteR = postCDeleteR
postCDeleteR :: TermId -> SchoolId -> CourseShorthand -> Handler Html postCDeleteR tid ssh csh = do
postCDeleteR = error "TODO: implement getCDeleteR" Entity cId _ <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh
deleteR DeleteRoute
{ drRecords = Set.singleton cId
, drRenderRecord = \(Entity _ Course{courseName, courseTerm, courseSchool}) -> do
School{schoolName} <- getJust courseSchool
return [whamlet|
#{courseName} (_{SomeMessage $ ShortTermIdentifier (unTermKey courseTerm)}, #{schoolName})
|]
, drRecordConfirmString = \(Entity _ Course{courseShorthand, courseTerm, courseSchool}) ->
return [st|#{unSchoolKey courseSchool}/#{termToText (unTermKey courseTerm)}/#{courseShorthand}|]
, drCaption = SomeMessage MsgCourseDeleteQuestion
, drSuccessMessage = SomeMessage MsgCourseDeleted
, drAbort = SomeRoute $ CourseR tid ssh csh CShowR
, drSuccess = SomeRoute CourseListR
}
{- TODO {- TODO
| False -- DELETE -- TODO: This no longer works that way!!! See new way in Handler.Term.termEditHandler | False -- DELETE -- TODO: This no longer works that way!!! See new way in Handler.Term.termEditHandler
, Just cid <- cfCourseId res -> do , Just cid <- cfCourseId res -> do

View File

@ -7,6 +7,7 @@ import Handler.Utils
-- import Handler.Utils.Zip -- import Handler.Utils.Zip
import Handler.Utils.Table.Cells import Handler.Utils.Table.Cells
import Handler.Utils.SheetType import Handler.Utils.SheetType
import Handler.Utils.Delete
-- import Data.Time -- import Data.Time
-- import qualified Data.Text as T -- import qualified Data.Text as T
@ -528,30 +529,26 @@ handleSheetEdit tid ssh csh msId template dbAction = do
$(widgetFile "formPageI18n") $(widgetFile "formPageI18n")
getSDelR, postSDelR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
getSDelR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html getSDelR = postSDelR
getSDelR tid ssh csh shn = do postSDelR tid ssh csh shn = do
((result,formWidget), formEnctype) <- runFormPost (buttonForm :: Form BtnDelete) sid <- runDB $ fetchSheetId tid ssh csh shn
case result of deleteR DeleteRoute
(FormSuccess BtnAbort) -> redirectUltDest $ CSheetR tid ssh csh shn SShowR { drRecords = Set.singleton sid
(FormSuccess BtnDelete) -> do , drRenderRecord = \(Entity _ Sheet{sheetName, sheetCourse}) -> do
runDB $ fetchSheetId tid ssh csh shn >>= deleteCascade Course{courseTerm, courseSchool, courseName} <- getJust sheetCourse
-- TODO: deleteCascade löscht aber nicht die hochgeladenen Dateien!!! School{schoolName} <- getJust courseSchool
addMessageI Info $ MsgSheetDelOk tid ssh csh shn return [whamlet|
redirect $ CourseR tid ssh csh SheetListR #{sheetName} (_{ShortTermIdentifier (unTermKey courseTerm)}, #{schoolName}, #{courseName})
_other -> do |]
submissionno <- runDB $ do , drRecordConfirmString = \(Entity _ Sheet{sheetName, sheetCourse}) -> do
sid <- fetchSheetId tid ssh csh shn Course{courseTerm, courseSchool, courseShorthand} <- getJust sheetCourse
count [SubmissionSheet ==. sid] return [st|#{termToText (unTermKey courseTerm)}/#{unSchoolKey courseSchool}/#{courseShorthand}/#{sheetName}|]
let formText = Just $ MsgSheetDelText submissionno , drCaption = SomeMessage MsgSheetDeleteQuestion
let actionUrl = CSheetR tid ssh csh shn SDelR , drSuccessMessage = SomeMessage MsgSheetDeleted
defaultLayout $ do , drAbort = SomeRoute $ CSheetR tid ssh csh shn SShowR
setTitleI $ MsgSheetTitle tid ssh csh shn , drSuccess = SomeRoute $ CourseR tid ssh csh SheetListR
$(widgetFile "formPageI18n") }
postSDelR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
postSDelR = getSDelR
insertSheetFile :: SheetId -> SheetFileType -> FileInfo -> YesodDB UniWorX () insertSheetFile :: SheetId -> SheetFileType -> FileInfo -> YesodDB UniWorX ()

View File

@ -59,7 +59,7 @@ postMessageR cID = do
<*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") (Just systemMessageTranslationContent) <*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") (Just systemMessageTranslationContent)
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") (Just systemMessageTranslationSummary) <*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") (Just systemMessageTranslationSummary)
) )
<*> combinedButtonField (universeF :: [BtnSubmitDelete]) <*> combinedButtonFieldF ""
let modifyTranss = Map.map (view $ _1._1) modifyTranss' let modifyTranss = Map.map (view $ _1._1) modifyTranss'

View File

@ -0,0 +1,73 @@
module Handler.Utils.Delete
( DeleteRoute(..)
, deleteR
) where
import Import
import Handler.Utils.Form
import Utils.Lens
import qualified Data.Text as Text
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
data DeleteRoute = forall record. (DeleteCascade record SqlBackend, Hashable (Key record)) => DeleteRoute
{ drRecords :: Set (Key record)
, drRenderRecord :: Entity record -> ReaderT SqlBackend (HandlerT UniWorX IO) Widget
, drRecordConfirmString :: Entity record -> ReaderT SqlBackend (HandlerT UniWorX IO) Text
, drCaption
, drSuccessMessage :: SomeMessage UniWorX
, drAbort
, drSuccess :: SomeRoute UniWorX
}
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
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 ""
formResult deleteFormRes $ \case
(_, catMaybes -> [BtnAbort]) ->
redirect drAbort
(inpConfirmStr, catMaybes -> [BtnDelete])
| ((==) `on` map CI.mk . Text.words) confirmString inpConfirmStr
-> do
runDB $ do
forM_ drRecords deleteCascade
addMessageI Success drSuccessMessage
redirect drSuccess
| otherwise
-> addMessageI Error MsgDeleteConfirmationWrong
_other -> return ()
Just targetRoute <- getCurrentRoute
defaultLayout
$(widgetFile "widgets/delete-confirmation")

View File

@ -56,6 +56,9 @@ import Data.Aeson.Text (encodeToLazyText)
data BtnDelete = BtnDelete | BtnAbort data BtnDelete = BtnDelete | BtnAbort
deriving (Enum, Eq, Ord, Bounded, Read, Show) deriving (Enum, Eq, Ord, Bounded, Read, Show)
instance Universe BtnDelete
instance Finite BtnDelete
instance PathPiece BtnDelete where -- for displaying the button only, not really for paths instance PathPiece BtnDelete where -- for displaying the button only, not really for paths
toPathPiece = showToPathPiece toPathPiece = showToPathPiece
fromPathPiece = readFromPathPiece fromPathPiece = readFromPathPiece

View File

@ -498,6 +498,12 @@ partitionM crit = ofoldlM dist mempty
| okay -> acc `mappend` (opoint x, mempty) | okay -> acc `mappend` (opoint x, mempty)
| otherwise -> acc `mappend` (mempty, opoint x) | otherwise -> acc `mappend` (mempty, opoint x)
mconcatMapM :: (Monoid b, Monad m, Foldable f) => (a -> m b) -> f a -> m b
mconcatMapM f = foldM (\x my -> mappend x <$> my) mempty . map f . Fold.toList
mconcatForM :: (Monoid b, Monad m, Foldable f) => f a -> (a -> m b) -> m b
mconcatForM = flip mconcatMapM
-------------- --------------
-- Sessions -- -- Sessions --

View File

@ -172,6 +172,7 @@ data FormIdentifier
| FIDSystemMessageAddTranslation | FIDSystemMessageAddTranslation
| FIDDBTableFilter | FIDDBTableFilter
| FIDDBTablePagesize | FIDDBTablePagesize
| FIDDelete
deriving (Eq, Ord, Read, Show) deriving (Eq, Ord, Read, Show)
instance PathPiece FormIdentifier where instance PathPiece FormIdentifier where
@ -230,13 +231,30 @@ buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
| otherwise = return $ Left "Wrong button value" | otherwise = return $ Left "Wrong button value"
fieldParse _ _ = return $ Left "Multiple button values" fieldParse _ _ = return $ Left "Multiple button values"
combinedButtonField :: (Button site a, Show (ButtonCssClass site)) => [a] -> AForm (HandlerT site IO) [Maybe a] combinedButtonField :: (Button site a, Show (ButtonCssClass site)) => [a] -> FieldSettings site -> AForm (HandlerT site IO) [Maybe a]
combinedButtonField = traverse b2f combinedButtonField bs FieldSettings{..} = formToAForm $ do
where mr <- getMessageRender
b2f b = aopt (buttonField b) "" Nothing fvId <- maybe newIdent return fsId
name <- maybe newIdent return fsName
(ress, fvs) <- fmap unzip . for bs $ \b -> mopt (buttonField b) ("" { fsId = Just $ fvId <> "__" <> toPathPiece b
, fsName = Just $ name <> "__" <> toPathPiece b
}) Nothing
return ( sequenceA ress
, pure FieldView
{ fvLabel = toHtml $ mr fsLabel
, fvTooltip = fmap (toHtml . mr) fsTooltip
, fvId
, fvInput = foldMap fvInput fvs
, fvErrors = bool Nothing (Just $ foldMap (fromMaybe mempty . fvErrors) fvs) $ any (isJust . fvErrors) fvs
, fvRequired = False
}
)
combinedButtonFieldF :: forall site a. (Button site a, Show (ButtonCssClass site), Finite a) => FieldSettings site -> AForm (HandlerT site IO) [Maybe a]
combinedButtonFieldF = combinedButtonField (universeF :: [a])
submitButton :: (Button site SubmitButton, Show (ButtonCssClass site)) => AForm (HandlerT site IO) () submitButton :: (Button site SubmitButton, Show (ButtonCssClass site)) => AForm (HandlerT site IO) ()
submitButton = void $ combinedButtonField [BtnSubmit] submitButton = void $ combinedButtonField [BtnSubmit] ""
autosubmitButton :: (Button site SubmitButton, Show (ButtonCssClass site)) => AForm (HandlerT site IO) () autosubmitButton :: (Button site SubmitButton, Show (ButtonCssClass site)) => AForm (HandlerT site IO) ()
autosubmitButton = void $ aopt (buttonField BtnSubmit) ("" & addAutosubmit) Nothing autosubmitButton = void $ aopt (buttonField BtnSubmit) ("" & addAutosubmit) Nothing

View File

@ -0,0 +1,13 @@
<p>_{drCaption}
<ul>
$forall (wdgt, _) <- sTargets
<li>
^{wdgt}
<p>_{SomeMessage $ MsgDeleteCopyStringIfSure (Set.size drRecords)}
<p .confirmationText>
#{confirmString}
<form method=POST action=@{targetRoute} enctype=#{deleteFormEnctype}>
^{deleteFormWdgt}

View File

@ -0,0 +1,5 @@
.confirmationText {
white-space: pre-wrap;
font-size: 14px;
font-family: monospace;
}