Multiple submission upload

This commit is contained in:
Gregor Kleen 2017-10-16 15:52:20 +02:00
parent b0e5f54b4d
commit 377d8667c2
7 changed files with 70 additions and 7 deletions

View File

@ -70,6 +70,7 @@ dependencies:
- mime-types - mime-types
- generic-deriving - generic-deriving
- blaze-html - blaze-html
- conduit-resumablesink >=0.2
# The library contains all of our application code. The executable # The library contains all of our application code. The executable
# defined below is just a thin wrapper. # defined below is just a thin wrapper.

2
routes
View File

@ -18,7 +18,7 @@
/course/#TermIdentifier/#Text/edit CourseEditExistR GET /course/#TermIdentifier/#Text/edit CourseEditExistR GET
/course/#TermIdentifier/#Text/show CourseShowR GET POST /course/#TermIdentifier/#Text/show CourseShowR GET POST
/submission SubmissionListR GET /submission SubmissionListR GET POST
/submission/#CryptoUUIDSubmission SubmissionR GET POST /submission/#CryptoUUIDSubmission SubmissionR GET POST
/submissions.zip SubmissionDownloadMultiArchiveR POST /submissions.zip SubmissionDownloadMultiArchiveR POST
!/submission/archive/#FilePath SubmissionDownloadArchiveR GET !/submission/archive/#FilePath SubmissionDownloadArchiveR GET

View File

@ -7,10 +7,11 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-}
module Handler.Submission where module Handler.Submission where
import Import import Import hiding (joinPath)
import Yesod.Form.Bootstrap3 import Yesod.Form.Bootstrap3
@ -19,6 +20,8 @@ import Handler.Utils
import Network.Mime import Network.Mime
import Control.Monad.Trans.Maybe import Control.Monad.Trans.Maybe
import Control.Monad.State.Class
import Control.Monad.Trans.State.Strict (StateT)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
@ -30,6 +33,10 @@ import qualified CryptoID.Base32 as Base32
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.List as Conduit
import Data.Conduit.ResumableSink
import Data.Map (Map)
import qualified Data.Map as Map
import System.FilePath import System.FilePath
@ -66,9 +73,49 @@ submissionTable = do
headedRowSelector toExternal fromExternal (HA.class_ "table table-striped table-hover") colonnade cryptedSubs headedRowSelector toExternal fromExternal (HA.class_ "table table-striped table-hover") colonnade cryptedSubs
getSubmissionListR :: Handler Html getSubmissionListR, postSubmissionListR :: Handler Html
getSubmissionListR = do getSubmissionListR = postSubmissionListR
(submissionTable, selectEncoding) <- generateFormPost . withFragment $ submissionTable postSubmissionListR = do
((uploadResult, uploadWidget), uploadEnctype) <- runFormPost . renderBootstrap3 BootstrapBasicForm $ (,)
<$> areq checkBoxField (bfs ("Dies sind Korrekturen" :: Text)) (Just False)
<*> fileAFormReq (bfs ("Archiv" :: Text))
<* bootstrapSubmit ("Mehrere Hochladen" :: BootstrapSubmit Text)
runDB $ do
case uploadResult of
FormMissing -> return ()
FormFailure _ -> setMessage "Bitte Eingabe korrigieren."
FormSuccess (isUpdate, fInfo) -> do
userId <- lift requireAuthId
let feed :: SubmissionId -> SubmissionContent -> StateT (Map SubmissionId (ResumableSink SubmissionContent (YesodDB UniWorX) SubmissionId)) (YesodDB UniWorX) ()
feed sId val = do
mSink <- gets $ Map.lookup sId
sink <- case mSink of
Just sink -> return sink
Nothing -> do
Submission{..} <- lift $ get404 sId
return . newResumableSink $ sinkSubmission submissionSheetId userId (Just (sId, isUpdate))
sink' <- lift $ yield val ++$$ sink
case sink' of
Left _ -> error "sinkSubmission returned prematurely"
Right nSink -> modify $ Map.insert sId nSink
sinkSubmissions :: Sink SubmissionContent (YesodDB UniWorX) ()
sinkSubmissions = do
sinks <- execStateC Map.empty . awaitForever $ \case
v@(Right (sId, _)) -> lift $ feed sId v
(Left f@File{..}) -> case splitDirectories fileTitle of
(cID:rest)
| not (null rest) -> do
cIDKey <- getsYesod appCryptoIDKey
sId <- Base32.decrypt cIDKey (CryptoID . CI.mk $ Text.pack cID :: CryptoFileNameSubmission)
lift . feed sId $ Left f{ fileTitle = joinPath rest }
| otherwise -> return ()
[] -> invalidArgs ["Encountered file/directory with empty name"]
lift $ mapM_ (void . closeResumableSink) sinks
runConduit $ fileSource fInfo =$= void consumeZip =$= extractRatings =$= void sinkSubmissions
(submissionTable, selectEncoding) <- generateFormPost . identifyForm "selection" . withFragment $ submissionTable
defaultLayout $(widgetFile "submission-list") defaultLayout $(widgetFile "submission-list")
@ -201,6 +248,7 @@ postSubmissionR cID = do
files <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do files <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFileId) E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFileId)
E.where_ (sf E.^. SubmissionFileSubmissionId E.==. E.val submissionId)
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate]
return (f, sf) return (f, sf)
return (submission, files) return (submission, files)

View File

@ -200,7 +200,7 @@ isRatingFile fName
decryptErrors err = throwM err decryptErrors err = throwM err
isRatingFile' :: FilePath -> Maybe CryptoFileNameSubmission isRatingFile' :: FilePath -> Maybe CryptoFileNameSubmission
isRatingFile' (normalise -> fName) isRatingFile' (takeFileName -> fName)
| (bName, ".txt") <- splitExtension fName | (bName, ".txt") <- splitExtension fName
, Just (CI.mk . Text.pack -> ciphertext) <- stripPrefix "bewertung_" bName , Just (CI.mk . Text.pack -> ciphertext) <- stripPrefix "bewertung_" bName
= Just CryptoID{..} = Just CryptoID{..}

View File

@ -6,6 +6,7 @@
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-}
@ -30,6 +31,8 @@ import qualified Database.Esqueleto as E
import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.List as Conduit
import qualified Data.Text as Text
data SubmissionSinkState = SubmissionSinkState data SubmissionSinkState = SubmissionSinkState
{ sinkSeenRating :: Any { sinkSeenRating :: Any
@ -85,6 +88,8 @@ sinkSubmission sheetId userId mExists = do
-> Sink SubmissionContent (YesodDB UniWorX) () -> Sink SubmissionContent (YesodDB UniWorX) ()
sinkSubmission' submissionId isUpdate = lift . finalize <=< execStateLC mempty . Conduit.mapM_ $ \case sinkSubmission' submissionId isUpdate = lift . finalize <=< execStateLC mempty . Conduit.mapM_ $ \case
Left file@(File{..}) -> do Left file@(File{..}) -> do
$logDebugS "sinkSubmission" . tshow $ (submissionId, fileTitle)
alreadySeen <- gets (Set.member fileTitle . sinkFilenames) alreadySeen <- gets (Set.member fileTitle . sinkFilenames)
when alreadySeen . throwM $ DuplicateFileTitle fileTitle when alreadySeen . throwM $ DuplicateFileTitle fileTitle
tell $ mempty{ sinkFilenames = Set.singleton fileTitle } tell $ mempty{ sinkFilenames = Set.singleton fileTitle }
@ -148,6 +153,8 @@ sinkSubmission sheetId userId mExists = do
lift $ deleteWhere [ SubmissionFileId <-. [ sfId | (_, Entity sfId sf) <- collidingFiles, submissionFileIsDeletion sf ] ] lift $ deleteWhere [ SubmissionFileId <-. [ sfId | (_, Entity sfId sf) <- collidingFiles, submissionFileIsDeletion sf ] ]
Right (submissionId', Rating'{..}) -> do Right (submissionId', Rating'{..}) -> do
$logDebugS "sinkSubmission" $ tshow submissionId'
unless (submissionId' == submissionId) $ throwM ForeignRating unless (submissionId' == submissionId) $ throwM ForeignRating
alreadySeen <- gets $ getAny . sinkSeenRating alreadySeen <- gets $ getAny . sinkSeenRating

View File

@ -6,6 +6,10 @@ nix:
extra-package-dbs: [] extra-package-dbs: []
packages: packages:
- . - .
- location:
git: https://github.com/pngwjpgh/conduit-resumablesink.git
commit: ee0679dc31cdcedb2dad240b941262b9dc16f06d
extra-dep: true
extra-deps: extra-deps:
- colonnade-1.1.1 - colonnade-1.1.1
- yesod-colonnade-1.1.0 - yesod-colonnade-1.1.0

View File

@ -1,3 +1,6 @@
<form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}>
^{uploadWidget}
<form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}> <form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}>
^{submissionTable} ^{submissionTable}
<button type=submit >Markierte herunterladen <button type=submit >Markierte herunterladen