MultiFileField Sheet

This commit is contained in:
Gregor Kleen 2018-04-03 14:51:25 +02:00
parent 7ee2aac209
commit 1b86abb46d
5 changed files with 141 additions and 24 deletions

View File

@ -34,5 +34,6 @@ instance PathPiece UUID where
decCryptoIDs [ ''SubmissionId decCryptoIDs [ ''SubmissionId
, ''CourseId , ''CourseId
, ''SheetId , ''SheetId
, ''FileId
] ]
{- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -} {- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -}

View File

@ -20,7 +20,7 @@ import Handler.Utils.Zip
import qualified Data.Text as T import qualified Data.Text as T
-- import Data.Function ((&)) -- import Data.Function ((&))
-- --
import Colonnade hiding (fromMaybe) import Colonnade hiding (fromMaybe, singleton)
import Yesod.Colonnade import Yesod.Colonnade
-- --
import qualified Data.UUID.Cryptographic as UUID import qualified Data.UUID.Cryptographic as UUID
@ -29,8 +29,12 @@ import qualified Data.Conduit.List as C
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E import qualified Database.Esqueleto.Internal.Sql as E
import Control.Monad.Writer (MonadWriter(..), execWriterT)
import Network.Mime import Network.Mime
import qualified Data.Set as Set
instance Eq (Unique Sheet) where instance Eq (Unique Sheet) where
(CourseSheet cid1 name1) == (CourseSheet cid2 name2) = (CourseSheet cid1 name1) == (CourseSheet cid2 name2) =
@ -51,7 +55,7 @@ data SheetForm = SheetForm
, sfVisibleFrom :: Maybe UTCTime , sfVisibleFrom :: Maybe UTCTime
, sfActiveFrom :: UTCTime , sfActiveFrom :: UTCTime
, sfActiveTo :: UTCTime , sfActiveTo :: UTCTime
, sfSheetF :: Maybe (Source Handler File) , sfSheetF :: Maybe (Source Handler (Either FileId File))
, sfHintFrom :: Maybe UTCTime , sfHintFrom :: Maybe UTCTime
, sfHintF :: Maybe FileInfo , sfHintF :: Maybe FileInfo
, sfSolutionFrom :: Maybe UTCTime , sfSolutionFrom :: Maybe UTCTime
@ -60,10 +64,16 @@ data SheetForm = SheetForm
} }
makeSheetForm :: Maybe SheetForm -> Form SheetForm makeSheetForm :: Maybe SheetId -> Maybe SheetForm -> Form SheetForm
makeSheetForm template = identForm FIDsheet $ \html -> do makeSheetForm msId template = identForm FIDsheet $ \html -> do
-- TODO: Yesod.Form.MassInput.inputList arbeitet Server-seitig :( let oldFileIds fType
-- Erstmal nur mit ZIP arbeiten | Just sId <- msId = fmap setFromList . fmap (map E.unValue) . runDB . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFileId
E.where_ $ sheetFile E.^. SheetFileSheetId E.==. E.val sId
E.&&. sheetFile E.^. SheetFileType E.==. E.val fType
return (file E.^. FileId)
| otherwise = return Set.empty
(result, widget) <- flip (renderAForm FormStandard) html $ SheetForm (result, widget) <- flip (renderAForm FormStandard) html $ SheetForm
<$> areq textField (fsb "Name") (sfName <$> template) <$> areq textField (fsb "Name") (sfName <$> template)
<*> aopt htmlField (fsb "Hinweise für Teilnehmer") (sfDescription <$> template) <*> aopt htmlField (fsb "Hinweise für Teilnehmer") (sfDescription <$> template)
@ -73,7 +83,7 @@ makeSheetForm template = identForm FIDsheet $ \html -> do
<*> aopt utcTimeField (fsb "Sichtbar ab") (sfVisibleFrom <$> template) <*> aopt utcTimeField (fsb "Sichtbar ab") (sfVisibleFrom <$> template)
<*> areq utcTimeField (fsb "Abgabe ab") (sfActiveFrom <$> template) <*> areq utcTimeField (fsb "Abgabe ab") (sfActiveFrom <$> template)
<*> areq utcTimeField (fsb "Abgabefrist") (sfActiveTo <$> template) <*> areq utcTimeField (fsb "Abgabefrist") (sfActiveTo <$> template)
<*> aopt multiFileField (fsb "Aufgabenstellung") (error "No defaults for file uploads") <*> aopt (multiFileField $ oldFileIds SheetExercise) (fsb "Aufgabenstellung") (sfSheetF <$> template)
<*> aopt utcTimeField (fsb "Hinweis ab") (sfHintFrom <$> template) <*> aopt utcTimeField (fsb "Hinweis ab") (sfHintFrom <$> template)
<*> fileAFormOpt (fsb "Hinweis") <*> fileAFormOpt (fsb "Hinweis")
<*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template) <*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template)
@ -250,7 +260,7 @@ getSheetNewR tid csh = do
let template = Nothing -- TODO: provide convenience by interpolating name/nr/dates+7days let template = Nothing -- TODO: provide convenience by interpolating name/nr/dates+7days
let action newSheet = -- More specific error message for new sheet could go here, if insertUnique returns Nothing let action newSheet = -- More specific error message for new sheet could go here, if insertUnique returns Nothing
insertUnique $ newSheet insertUnique $ newSheet
handleSheetEdit tid csh template action handleSheetEdit tid csh Nothing template action
postSheetNewR :: TermId -> Text -> Handler Html postSheetNewR :: TermId -> Text -> Handler Html
postSheetNewR = getSheetNewR postSheetNewR = getSheetNewR
@ -258,7 +268,14 @@ postSheetNewR = getSheetNewR
getSheetEditR :: TermId -> Text -> Text -> Handler Html getSheetEditR :: TermId -> Text -> Text -> Handler Html
getSheetEditR tid csh shn = do getSheetEditR tid csh shn = do
sheetEnt <- runDB $ fetchSheet tid csh shn (sheetEnt, sheetFileIds) <- runDB $ do
ent <- fetchSheet tid csh shn
fIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFileId
E.where_ $ sheetFile E.^. SheetFileSheetId E.==. E.val (entityKey ent)
E.&&. sheetFile E.^. SheetFileType E.==. E.val SheetExercise
return (file E.^. FileId)
return (ent, fIds)
let sid = entityKey sheetEnt let sid = entityKey sheetEnt
let oldSheet@(Sheet {..}) = entityVal sheetEnt let oldSheet@(Sheet {..}) = entityVal sheetEnt
let template = Just $ SheetForm let template = Just $ SheetForm
@ -270,7 +287,7 @@ getSheetEditR tid csh shn = do
, sfVisibleFrom = sheetVisibleFrom , sfVisibleFrom = sheetVisibleFrom
, sfActiveFrom = sheetActiveFrom , sfActiveFrom = sheetActiveFrom
, sfActiveTo = sheetActiveTo , sfActiveTo = sheetActiveTo
, sfSheetF = Nothing -- TODO , sfSheetF = Just . yieldMany . map Left $ Set.toList sheetFileIds
, sfHintFrom = sheetHintFrom , sfHintFrom = sheetHintFrom
, sfHintF = Nothing -- TODO , sfHintF = Nothing -- TODO
, sfSolutionFrom = sheetSolutionFrom , sfSolutionFrom = sheetSolutionFrom
@ -283,17 +300,17 @@ getSheetEditR tid csh shn = do
case replaceRes of case replaceRes of
Nothing -> return $ Just sid Nothing -> return $ Just sid
(Just _err) -> return $ Nothing -- More specific error message for edit old sheet could go here (Just _err) -> return $ Nothing -- More specific error message for edit old sheet could go here
handleSheetEdit tid csh template action handleSheetEdit tid csh (Just sid) template action
postSheetEditR :: TermId -> Text -> Text -> Handler Html postSheetEditR :: TermId -> Text -> Text -> Handler Html
postSheetEditR = getSheetEditR postSheetEditR = getSheetEditR
handleSheetEdit :: TermId -> Text -> Maybe SheetForm -> (Sheet -> YesodDB UniWorX (Maybe SheetId)) -> Handler Html handleSheetEdit :: TermId -> Text -> Maybe SheetId -> Maybe SheetForm -> (Sheet -> YesodDB UniWorX (Maybe SheetId)) -> Handler Html
handleSheetEdit tid csh template dbAction = do handleSheetEdit tid csh msId template dbAction = do
let tident = unTermKey tid let tident = unTermKey tid
let mbshn = sfName <$> template let mbshn = sfName <$> template
aid <- requireAuthId aid <- requireAuthId
((res,formWidget), formEnctype) <- runFormPost $ makeSheetForm template ((res,formWidget), formEnctype) <- runFormPost $ makeSheetForm msId template
case res of case res of
(FormSuccess SheetForm{..}) -> do (FormSuccess SheetForm{..}) -> do
saveOkay <- runDB $ do saveOkay <- runDB $ do
@ -374,10 +391,17 @@ insertSheetFile sid ftype finfo = do
fid <- insert file fid <- insert file
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step
insertSheetFile' :: SheetId -> SheetFileType -> Source Handler File -> YesodDB UniWorX () insertSheetFile' :: SheetId -> SheetFileType -> Source Handler (Either FileId File) -> YesodDB UniWorX ()
insertSheetFile' sid ftype fs = do insertSheetFile' sid ftype fs = do
runConduit $ transPipe lift fs =$= C.mapM_ finsert oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFileId
E.where_ $ sheetFile E.^. SheetFileSheetId E.==. E.val sid
E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype
return (file E.^. FileId)
keep <- execWriterT . runConduit $ transPipe (lift . lift) fs =$= C.mapM_ finsert
mapM_ deleteCascade $ (oldFileIds \\ keep :: Set FileId)
where where
finsert file = do finsert (Left fileId) = tell $ singleton fileId
finsert (Right file) = lift $ do
fid <- insert file fid <- insert file
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step

View File

@ -8,9 +8,13 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE LambdaCase #-}
module Handler.Utils.Form where module Handler.Utils.Form where
import Handler.Utils.Form.Types
import Import import Import
import qualified Data.Char as Char import qualified Data.Char as Char
import Handler.Utils.DateTime import Handler.Utils.DateTime
@ -31,6 +35,11 @@ import Web.PathPieces (showToPathPiece, readFromPathPiece)
import Handler.Utils.Zip import Handler.Utils.Zip
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E
import qualified Data.Set as Set
------------------------------------------------ ------------------------------------------------
-- Unique Form Identifiers to avoid accidents -- -- Unique Form Identifiers to avoid accidents --
------------------------------------------------ ------------------------------------------------
@ -236,17 +245,54 @@ schoolEntField = selectField schools
where where
schools = optionsPersist [] [Asc SchoolName] schoolName schools = optionsPersist [] [Asc SchoolName] schoolName
multiFileField :: Field Handler (Source Handler File) multiFileField :: Handler (Set FileId) -> Field Handler (Source Handler (Either FileId File))
multiFileField = Field{..} multiFileField permittedFiles' = Field{..}
where where
fieldEnctype = Multipart fieldEnctype = Multipart
fieldParse vals files fieldParse vals files
| null files = return $ Right Nothing | null files
| [unpackZips] == vals = return . Right . Just $ mapM_ sourceFiles files , null vals = return $ Right Nothing
| otherwise = return . Right . Just $ C.sourceList files .| C.mapM acceptFile | otherwise = return . Right . Just $ do
fieldView fieldId fieldName attrs prev req = $(widgetFile "multiFileField") pVals <- lift permittedFiles'
let
decrypt' :: CryptoUUIDFile -> Handler (Maybe FileId)
decrypt' = fmap (either (\(_ :: CryptoIDError) -> Nothing) Just) . try . decrypt
yieldMany vals
.| C.filter (/= unpackZips)
.| C.map fromPathPiece .| C.catMaybes
.| C.mapMaybeM decrypt'
.| C.filter (`elem` pVals)
.| C.map Left
let
handleFile :: FileInfo -> Source Handler File
handleFile
| doUnpack = sourceFiles
| otherwise = yieldM . acceptFile
mapM_ handleFile files .| C.map Right
where
doUnpack = unpackZips `elem` vals
fieldView fieldId fieldName attrs val req = do
pVals <- handlerToWidget permittedFiles'
sentVals <- for val $ \src -> handlerToWidget . sourceToList $ src .| takeLefts
let
toFUI (E.Value fuiId', E.Value fuiTitle) = do
fuiId <- encrypt fuiId'
fuiHtmlId <- newIdent
let fuiChecked
| Right sentVals' <- sentVals = fuiId' `elem` sentVals'
| otherwise = True
return FileUploadInfo{..}
fileInfos <- mapM toFUI <=< handlerToWidget . runDB . E.select . E.from $ \file -> do
E.where_ $ file E.^. FileId `E.in_` E.valList (setToList pVals)
E.orderBy [E.asc $ file E.^. FileTitle]
return (file E.^. FileId, file E.^. FileTitle)
$(widgetFile "multiFileField")
unpackZips :: Text unpackZips :: Text
unpackZips = "unpack-zip" unpackZips = "unpack-zip"
takeLefts :: Monad m => ConduitM (Either b a) b m ()
takeLefts = awaitForever $ \case
Right _ -> return ()
Left r -> yield r
sheetTypeAFormReq :: FieldSettings UniWorX -> Maybe SheetType -> AForm Handler SheetType sheetTypeAFormReq :: FieldSettings UniWorX -> Maybe SheetType -> AForm Handler SheetType
sheetTypeAFormReq d Nothing = sheetTypeAFormReq d Nothing =
@ -338,3 +384,25 @@ setTooltip :: String -> FieldSettings site -> FieldSettings site
setTooltip tt fs setTooltip tt fs
| null tt = fs { fsTooltip = Nothing } | null tt = fs { fsTooltip = Nothing }
| otherwise = fs { fsTooltip = Just $ fromString tt } | otherwise = fs { fsTooltip = Just $ fromString tt }
optionsPersistCryptoId :: forall site backend a msg.
( YesodPersist site
, PersistQueryRead backend
, HasCryptoUUID (Key a) (HandlerT site IO)
, RenderMessage site msg
, YesodPersistBackend site ~ backend
, PersistRecordBackend a backend
)
=> [Filter a]
-> [SelectOpt a]
-> (a -> msg)
-> HandlerT site IO (OptionList (Key a))
optionsPersistCryptoId filts ords toDisplay = fmap mkOptionList $ do
mr <- getMessageRender
pairs <- runDB $ selectList filts ords
cPairs <- forM pairs $ \e@(Entity key _) -> (,) <$> encrypt key <*> pure e
return $ map (\(cId, Entity key value) -> Option
{ optionDisplay = mr (toDisplay value)
, optionInternalValue = key
, optionExternalValue = toPathPiece (cId :: CryptoID UUID (Key a))
}) cPairs

View File

@ -0,0 +1,12 @@
{-# LANGUAGE NoImplicitPrelude #-}
module Handler.Utils.Form.Types where
import Import
data FileUploadInfo = FileUploadInfo
{ fuiId :: CryptoUUIDFile
, fuiTitle :: FilePath
, fuiHtmlId :: Text
, fuiChecked :: Bool
}

View File

@ -1,5 +1,17 @@
$newline never $newline never
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips} :req:required> <input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips} :req:required>
<label for=#{fieldId}_zip> <label for=#{fieldId}_zip>
ZIPs entpacken ZIPs entpacken
<input type=file id=#{fieldId} name=#{fieldName}>
<ul>
$forall FileUploadInfo{..} <- fileInfos
<li>
<input type=checkbox name=#{fieldName} value=#{toPathPiece fuiId} id=#{fuiHtmlId} :fuiChecked:checked>
<span style="display:none">
#{fuiTitle}
<label for=#{fuiHtmlId}>
#{fuiTitle}
<li>
<input type=file id=#{fieldId} name=#{fieldName} multiple>