last edited by #{name} at #{formatTimeGerDTlong time}
$maybe fileTable <- mFileTable
@@ -274,26 +276,15 @@ submissionHelper tid csh shn (SubmissionMode mcid) = do
|]
-
-
-
-submissionFileSource :: SubmissionId -> Source (YesodDB UniWorX) (Entity File)
-submissionFileSource = E.selectSource . E.from . submissionFileQuery
-
-submissionFileQuery :: SubmissionId -> E.SqlExpr (Entity SubmissionFile) `E.InnerJoin` E.SqlExpr (Entity File)
- -> E.SqlQuery (E.SqlExpr (Entity File))
-submissionFileQuery submissionID (sf `E.InnerJoin` f) = E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
- E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
- E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
- E.where_ . E.not_ $ sf E.^. SubmissionFileIsDeletion -- TODO@gk: won't work as intended! Fix with refactor
- E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] -- E.desc returns corrector updated data first
- return f
-
-getSubmissionDownloadSingleR :: CryptoFileNameSubmission -> FilePath -> Handler TypedContent
-getSubmissionDownloadSingleR cID path = do
+getSubmissionDownloadSingleR :: TermId -> Text -> Text -> CryptoFileNameSubmission -> FilePath -> Handler TypedContent
+getSubmissionDownloadSingleR tid csh shn cID path = do
submissionID <- decrypt cID
runDB $ do
+ shid <- fetchSheetId tid csh shn
+ Submission{..} <- get404 submissionID
+ when (shid /= submissionSheet) $ invalidArgsI [MsgSubmissionWrongSheet]
+
isRating <- maybe False (== submissionID) <$> isRatingFile path
case isRating of
True -> do
@@ -311,202 +302,24 @@ getSubmissionDownloadSingleR cID path = do
let fileName = Text.pack $ takeFileName path
case results of
- [Entity _ File{ fileContent = Just c }] -> return $ TypedContent (defaultMimeLookup fileName) (toContent c)
+ [Entity _ File{ fileContent = Just c }] -> return $ TypedContent (defaultMimeLookup fileName <> "; charset=utf-8") (toContent c)
_ -> notFound
-getSubmissionDownloadArchiveR :: ZIPArchiveName SubmissionId -> Handler TypedContent
-getSubmissionDownloadArchiveR (ZIPArchiveName cID) = do
+getSubmissionDownloadArchiveR :: TermId -> Text -> Text -> ZIPArchiveName SubmissionId -> Handler TypedContent
+getSubmissionDownloadArchiveR tid csh shn (ZIPArchiveName cID) = do
submissionID <- decrypt cID
cUUID <- encrypt submissionID
respondSourceDB "application/zip" $ do
+ lift $ do
+ shid <- fetchSheetId tid csh shn
+ Submission{..} <- get404 submissionID
+ when (shid /= submissionSheet) $ invalidArgsI [MsgSubmissionWrongSheet]
+
rating <- lift $ getRating submissionID
case rating of
Nothing -> lift notFound
Just rating' -> do
let fileEntitySource' :: Source (YesodDB UniWorX) File
fileEntitySource' = submissionFileSource submissionID =$= Conduit.map entityVal >> yieldM (ratingFile cID rating')
- info = ZipInfo { zipComment = Text.encodeUtf8 . tshow $ ciphertext (cUUID :: CryptoUUIDSubmission) }
+ info = ZipInfo { zipComment = Text.encodeUtf8 . pack . CI.foldedCase $ ciphertext (cUUID :: CryptoFileNameSubmission) }
fileEntitySource' =$= produceZip info =$= Conduit.map toFlushBuilder
-
-
-
-
-
-
-
------------------------------------------------------------------------------------------------
-------------------------- DEMO BELOW
-
-
-submissionTable :: MForm Handler (FormResult [SubmissionId], Widget)
-submissionTable = do
- subs <- lift . runDB $ E.select . E.from $ \(sub `E.InnerJoin` sheet `E.InnerJoin` course) -> do
- E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
- E.on $ sheet E.^. SheetId E.==. sub E.^. SubmissionSheet
-
- return (sub, sheet, course)
-
- cryptedSubs <- forM subs $ \s@(Entity submissionId _, _, _) ->
- (,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s
-
- let
- anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseR courseTerm courseShorthand CShowR
- courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName
- anchorSubmission (_, cUUID, _) = SubmissionDemoR cUUID
- submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID
- colonnade = mconcat
- [ headed "Abgabe-ID" $ anchorCell anchorSubmission submissionText
- , headed "Kurs" $ anchorCell anchorCourse courseText
- , headed "Blatt" $ \(_, _, (_, Entity _ Sheet{..}, _)) -> textCell $ sheetName
- ]
- toExternal :: (CryptoFileNameSubmission, CryptoUUIDSubmission, a) -> Handler CryptoUUIDSubmission
- toExternal (_, cID, _) = return cID
- fromExternal :: CryptoUUIDSubmission -> Handler SubmissionId
- fromExternal = decrypt
- headedRowSelector toExternal fromExternal (HA.class_ "table table-striped table-hover") colonnade cryptedSubs
-
-
-getSubmissionListR, postSubmissionListR :: Handler Html
-getSubmissionListR = postSubmissionListR
-postSubmissionListR = do
- ((uploadResult, uploadWidget), uploadEnctype) <- runFormPost . renderAForm FormStandard $ (,)
- <$> areq checkBoxField "Dies sind Korrekturen" (Just False)
- <*> fileAFormReq "Archiv"
- <* submitButton
-
- runDB $ do
- case uploadResult of
- FormMissing -> return ()
- FormFailure _ -> addMessage "warning" "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 submissionSheet 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
- sId <- decrypt (CryptoID $ CI.mk 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
-
- (subTable, selectEncoding) <- generateFormPost . identifyForm "selection" . withFragment $ submissionTable
-
- defaultLayout $(widgetFile "submission-list")
-
-
-
-postSubmissionDownloadMultiArchiveR :: Handler TypedContent
-postSubmissionDownloadMultiArchiveR = do
- ((selectResult, _), _) <- runFormPost . withFragment $ submissionTable
-
- case selectResult of
- FormMissing -> invalidArgs ["Missing submission numbers"]
- FormFailure errs -> invalidArgs errs
- FormSuccess ids -> do
- (dbrunner, cleanup) <- getDBRunner
-
- ratedSubmissions <- runDBRunner dbrunner $ do
- submissions <- selectList [ SubmissionId <-. ids ] []
- forM submissions $ \s@(Entity submissionId _) -> maybe (invalidArgs ["Invalid submission numbers"]) (return . (, s)) =<< getRating submissionId
-
- (<* cleanup) . respondSource "application/zip" . transPipe (runDBRunner dbrunner) $ do
- let
- fileEntitySource' :: (Rating, Entity Submission) -> Source (YesodDB UniWorX) File
- fileEntitySource' (rating, Entity submissionID Submission{..}) = do
- cID <- encrypt submissionID
-
- let
- directoryName = CI.foldedCase $ ciphertext (cID :: CryptoFileNameSubmission)
-
- fileEntitySource = do
- submissionFileSource submissionID =$= Conduit.map entityVal
- yieldM (ratingFile cID rating)
-
- withinDirectory f@File{..} = f { fileTitle = directoryName > fileTitle }
-
- lastEditMb <- lift $ selectList [SubmissionEditSubmission ==. submissionID] [Desc SubmissionEditTime, LimitTo 1]
- lastEditTime <- case lastEditMb of
- [(submissionEditTime.entityVal -> time)] -> return time
- _other -> liftIO getCurrentTime
- yield $ File
- { fileModified = lastEditTime
- , fileTitle = directoryName
- , fileContent = Nothing
- }
-
- fileEntitySource =$= mapC withinDirectory
-
- mapM_ fileEntitySource' ratedSubmissions =$= produceZip def =$= Conduit.map toFlushBuilder
-
-
-
-
-
-getSubmissionDemoR, postSubmissionDemoR :: CryptoUUIDSubmission -> Handler Html
-getSubmissionDemoR = postSubmissionDemoR
-postSubmissionDemoR cID = do
- submissionId <- decrypt cID
-
- ((uploadResult, uploadWidget), uploadEnctype) <- runFormPost . renderAForm FormStandard $ (,)
- <$> areq checkBoxField "Dies ist eine Korrektur" (Just False)
- <*> fileAFormReq "Datei"
- <* submitButton
-
- (submission, files) <- runDB $ do
- submission <- do
- submission@Submission{..} <- get404 submissionId
- case uploadResult of
- FormMissing -> return submission
- FormFailure _ -> submission <$ addMessage "warning" "Bitte Eingabe korrigieren."
- FormSuccess (isUpdate, fInfo) -> do
- userId <- lift requireAuthId
- let mimeType = defaultMimeLookup (fileName fInfo)
- source
- | mimeType == "application/zip" = fileSource fInfo =$= void consumeZip
- | otherwise = do
- let fileTitle = Text.unpack $ fileName fInfo
- fileModified <- liftIO getCurrentTime
- yieldM $ do
- fileContent <- Just <$> runConduit (fileSource fInfo =$= foldC)
- return File{..}
- submissionId' <- runConduit $ source =$= extractRatings =$= sinkSubmission submissionSheet userId (Just (submissionId, isUpdate))
- get404 submissionId'
-
- 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.^. SubmissionFileFile)
- E.where_ (sf E.^. SubmissionFileSubmission E.==. E.val submissionId)
- E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate]
- return (f, sf)
- return (submission, files)
-
- let
- Rating'{..} = Rating'
- { ratingPoints = submissionRatingPoints submission
- , ratingComment = submissionRatingComment submission
- , ratingTime = submissionRatingTime submission
- }
-
- cID' <- encrypt submissionId
- let
- archiveBaseName = CI.foldedCase $ ciphertext (cID' :: CryptoFileNameSubmission)
- archiveName = archiveBaseName <.> "zip"
-
- defaultLayout $(widgetFile "submission")
diff --git a/src/Handler/Term.hs b/src/Handler/Term.hs
index 59c103e28..d847acbd3 100644
--- a/src/Handler/Term.hs
+++ b/src/Handler/Term.hs
@@ -7,6 +7,7 @@
, MultiParamTypeClasses
, TypeFamilies
, FlexibleContexts
+ , PartialTypeSignatures
#-}
module Handler.Term where
@@ -18,7 +19,6 @@ import Handler.Utils
import Yesod.Form.Bootstrap3
import Colonnade hiding (bool)
-import Yesod.Colonnade
import qualified Database.Esqueleto as E
@@ -41,7 +41,7 @@ getTermShowR = do
selectRep $ do
provideRep $ toJSON . map fst <$> runDB (E.select $ E.from termData)
provideRep $ do
- let colonnadeTerms = mconcat
+ let colonnadeTerms = widgetColonnade $ mconcat
[ sortable Nothing "Kürzel" $ \(Entity tid Term{..},_) -> cell $ do
-- Scrap this if to slow, create term edit page instead
adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False
@@ -52,21 +52,21 @@ getTermShowR = do
$else
#{termToText termName}
|]
- , sortable (Just "lecture-start") "Beginn Vorlesungen" $ \(Entity _ Term{..},_) ->
+ , sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureStart
, sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureEnd
, sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) ->
- textCell $ bool "" tickmark termActive
+ textCell $ (bool "" tickmark termActive :: Text)
, sortable Nothing "Kursliste" $ anchorCell
(\(Entity tid _, _) -> TermCourseListR tid)
(\(_, E.Value numCourses) -> [whamlet|_{MsgNumCourses numCourses}|])
, sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) ->
- stringCell $ formatTimeGerWD termStart
+ stringCell $ formatTimeGerWD termStart
, sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) ->
- stringCell $ formatTimeGerWD termEnd
+ stringCell $ formatTimeGerWD termEnd
, sortable Nothing "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
- stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays
+ stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays
]
table <- dbTable def $ DBTable
{ dbtSQLQuery = termData
diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs
index 6c9a3389b..770129424 100644
--- a/src/Handler/Utils/Form.hs
+++ b/src/Handler/Utils/Form.hs
@@ -40,15 +40,19 @@ import qualified Data.Conduit.List as C
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E
+import Data.Set (Set)
import qualified Data.Set as Set
+import Data.Map (Map)
+import qualified Data.Map as Map
+
import Control.Monad.Writer.Class
------------------------------------------------
-- Unique Form Identifiers to avoid accidents --
------------------------------------------------
-data FormIdentifier = FIDcourse | FIDsheet | FIDsubmission | FIDsettings
+data FormIdentifier = FIDcourse | FIDsheet | FIDsubmission | FIDsettings | FIDcorrectors | FIDcorrectorTable | FIDcorrectionsUpload
deriving (Enum, Eq, Ord, Bounded, Read, Show)
@@ -530,3 +534,16 @@ mforced Field{..} FieldSettings{..} val = do
aforced :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> a -> AForm m a
aforced field settings val = formToAForm $ second pure <$> mforced field settings val
+
+multiAction :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action)
+ => Map action (MForm (HandlerT UniWorX IO) (FormResult a, Maybe Widget))
+ -> MForm (HandlerT UniWorX IO) (FormResult a, Widget)
+multiAction acts = do
+ mr <- getMessageRender
+ let
+ options = OptionList [ Option (mr a) a (toPathPiece a) | a <- Map.keys acts ] fromPathPiece
+ (actionRes, actionView) <- mreq (selectField $ return options) "" Nothing
+ results <- sequence acts
+ let actionWidgets = Map.foldrWithKey (\act -> \case (_, Just w) -> ($(widgetFile "widgets/multiAction") :); (_, Nothing) -> id) [] results
+ actionResults = Map.map fst results
+ return ((actionResults Map.!) =<< actionRes, $(widgetFile "widgets/multiActionCollect"))
diff --git a/src/Handler/Utils/Submission.hs b/src/Handler/Utils/Submission.hs
index 7cfc97f4e..8cf22e67d 100644
--- a/src/Handler/Utils/Submission.hs
+++ b/src/Handler/Utils/Submission.hs
@@ -8,20 +8,26 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE PartialTypeSignatures #-}
module Handler.Utils.Submission
- ( SubmissionSinkException(..)
+ ( AssignSubmissionException(..)
+ , assignSubmissions
+ , submissionFileSource, submissionFileQuery
+ , submissionMultiArchive
+ , SubmissionSinkException(..)
, sinkSubmission
) where
-import Import hiding ((.=))
+import Import hiding ((.=), joinPath)
import Control.Lens
import Control.Lens.Extras (is)
import Utils.Lens
-import Control.Monad.State hiding (forM_)
+import Control.Monad.State hiding (forM_, mapM_,foldM)
import qualified Control.Monad.Random as Rand
import Data.Maybe
@@ -32,15 +38,21 @@ import qualified Data.Set as Set
import Data.Map (Map, (!?))
import qualified Data.Map as Map
+import Data.CaseInsensitive (CI)
+import qualified Data.CaseInsensitive as CI
import Data.Monoid (Monoid, Any(..))
import Generics.Deriving.Monoid (memptydefault, mappenddefault)
import Handler.Utils.Rating
+import Handler.Utils.Zip
import qualified Database.Esqueleto as E
import qualified Data.Conduit.List as Conduit
+import Data.Conduit.ResumableSink
+
+import System.FilePath
data AssignSubmissionException = NoCorrectorsByProportion
@@ -49,12 +61,12 @@ data AssignSubmissionException = NoCorrectorsByProportion
instance Exception AssignSubmissionException
-- | Assigns all submissions according to sheet corrector loads
-assignSubmissions ::
- SheetId -- ^ Sheet do distribute to correction
- -> YesodDB UniWorX (Set SubmissionId -- ^ assigned submissions
- ,Set SubmissionId -- ^ unassigend submissions (no tutors by load)
- )
-assignSubmissions sid = do
+assignSubmissions :: SheetId -- ^ Sheet do distribute to correction
+ -> Maybe (Set SubmissionId) -- ^ Optionally restrict submission to consider
+ -> YesodDB UniWorX ( Set SubmissionId -- ^ assigned submissions
+ , Set SubmissionId -- ^ unassigend submissions (no tutors by load)
+ )
+assignSubmissions sid restriction = do
correctors <- selectList [SheetCorrectorSheet ==. sid] []
let corrsGroup = filter hasTutorialLoad correctors -- needed as List within Esqueleto
let corrsProp = filter hasPositiveLoad correctors
@@ -74,7 +86,8 @@ assignSubmissions sid = do
E.where_ (tutorial E.^. TutorialTutor `E.in_` E.valList (map (sheetCorrectorUser . entityVal) corrsGroup))
return $ tutorial E.^. TutorialTutor
E.on $ user E.?. UserId `E.in_` E.justList tutors
- E.where_ $ submission E.^. SubmissionSheet E.==. E.val sid
+ E.where_ $ submission E.^. SubmissionSheet E.==. E.val sid
+ E.&&. maybe (E.val True) (submission E.^. SubmissionId `E.in_`) (E.valList . Set.toList <$> restriction)
E.orderBy [E.rand] -- randomize for fair tutor distribution
return (submission E.^. SubmissionId, user) -- , listToMaybe tutors)
@@ -105,6 +118,57 @@ assignSubmissions sid = do
hasTutorialLoad = isJust . byTutorial . sheetCorrectorLoad . entityVal
+submissionFileSource :: SubmissionId -> Source (YesodDB UniWorX) (Entity File)
+submissionFileSource = E.selectSource . E.from . submissionFileQuery
+
+submissionFileQuery :: SubmissionId -> E.SqlExpr (Entity SubmissionFile) `E.InnerJoin` E.SqlExpr (Entity File)
+ -> E.SqlQuery (E.SqlExpr (Entity File))
+submissionFileQuery submissionID (sf `E.InnerJoin` f) = E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
+ E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
+ E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
+ E.where_ . E.not_ $ sf E.^. SubmissionFileIsDeletion -- TODO@gk: won't work as intended! Fix with refactor
+ E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] -- E.desc returns corrector updated data first
+ return f
+
+submissionMultiArchive :: Set SubmissionId -> Handler TypedContent
+submissionMultiArchive (Set.toList -> ids) = do
+ (dbrunner, cleanup) <- getDBRunner
+
+ ratedSubmissions <- runDBRunner dbrunner $ do
+ submissions <- selectList [ SubmissionId <-. ids ] []
+ forM submissions $ \s@(Entity submissionId _) -> maybe (invalidArgs ["Invalid submission numbers"]) (return . (, s)) =<< getRating submissionId
+
+ (<* cleanup) . respondSource "application/zip" . transPipe (runDBRunner dbrunner) $ do
+ let
+ fileEntitySource' :: (Rating, Entity Submission) -> Source (YesodDB UniWorX) File
+ fileEntitySource' (rating, Entity submissionID Submission{..}) = do
+ cID <- encrypt submissionID
+
+ let
+ directoryName = CI.foldedCase $ ciphertext (cID :: CryptoFileNameSubmission)
+
+ fileEntitySource = do
+ submissionFileSource submissionID =$= Conduit.map entityVal
+ yieldM (ratingFile cID rating)
+
+ withinDirectory f@File{..} = f { fileTitle = directoryName > fileTitle }
+
+ lastEditMb <- lift $ selectList [SubmissionEditSubmission ==. submissionID] [Desc SubmissionEditTime, LimitTo 1]
+ lastEditTime <- case lastEditMb of
+ [(submissionEditTime.entityVal -> time)] -> return time
+ _other -> liftIO getCurrentTime
+ yield $ File
+ { fileModified = lastEditTime
+ , fileTitle = directoryName
+ , fileContent = Nothing
+ }
+
+ fileEntitySource =$= mapC withinDirectory
+
+ mapM_ fileEntitySource' ratedSubmissions =$= produceZip def =$= Conduit.map toFlushBuilder
+
+
+
data SubmissionSinkState = SubmissionSinkState
@@ -125,9 +189,9 @@ data SubmissionSinkException = DuplicateFileTitle FilePath
instance Exception SubmissionSinkException
-sinkSubmission :: SheetId
- -> UserId
- -> Maybe (SubmissionId, Bool {-^ Is this a correction -})
+sinkSubmission :: UserId
+ -> Either SheetId SubmissionId
+ -> Bool -- ^ Is this a correction
-> Sink SubmissionContent (YesodDB UniWorX) SubmissionId
-- ^ Replace the currently saved files for the given submission (either
-- corrected files or original ones, depending on arguments) with the supplied
@@ -137,25 +201,28 @@ sinkSubmission :: SheetId
-- are deleted (or marked as deleted in the case of this being a correction).
--
-- A 'Submission' is created if no 'SubmissionId' is supplied
-sinkSubmission sheetId userId mExists = do
- now <- liftIO getCurrentTime
- let
- submissionSheet = sheetId
- submissionRatingPoints = Nothing
- submissionRatingComment = Nothing
- submissionRatingBy = Nothing
- submissionRatingTime = Nothing
-
- (sId, isUpdate) <- lift $ maybe ((, False) <$> (insert Submission{..} >>= (\sid -> sid <$ insert (SubmissionEdit userId now sid)))) return mExists
-
+sinkSubmission userId mExists isUpdate = do
+ sId <- lift $ case mExists of
+ Left sheetId -> do
+ let
+ submissionSheet = sheetId
+ submissionRatingPoints = Nothing
+ submissionRatingComment = Nothing
+ submissionRatingBy = Nothing
+ submissionRatingTime = Nothing
+ sId <- insert Submission{..}
+ -- now <- liftIO getCurrentTime
+ -- insert $ SubmissionEdit userId now sId -- This is done automatically during 'sinkSubmission'' iff the given submission is nonempty
+ return sId
+ Right sId -> return sId
sId <$ sinkSubmission' sId isUpdate
where
tell = modify . mappend
sinkSubmission' :: SubmissionId
- -> Bool -- ^ Is this a correction
- -> Sink SubmissionContent (YesodDB UniWorX) ()
+ -> Bool -- ^ Is this a correction
+ -> Sink SubmissionContent (YesodDB UniWorX) ()
sinkSubmission' submissionId isUpdate = lift . finalize <=< execStateLC mempty . Conduit.mapM_ $ \case
Left file@(File{..}) -> do
$logDebugS "sinkSubmission" . tshow $ (submissionId, fileTitle)
@@ -303,3 +370,43 @@ sinkSubmission sheetId userId mExists = do
, SubmissionRatingBy =. Nothing
, SubmissionRatingComment =. Nothing
]
+
+sinkMultiSubmission :: UserId
+ -> Bool {-^ Are these corrections -}
+ -> Sink SubmissionContent (YesodDB UniWorX) (Set SubmissionId)
+
+-- ^ Expects all supplied 'SubmissionContent' to contain an encrypted 'SubmissionId' and replaces the currently saved files for the respective submissions (either corrected files or original ones, depending on arguments) with the supplied 'SubmissionContent'.
+--
+-- Files that don't occur in the 'SubmissionContent' but are in the database are deleted (or marked as deleted in the case of this being a correction).
+sinkMultiSubmission userId isUpdate = do
+ 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 userId (Right sId) isUpdate
+ sink' <- lift $ yield val ++$$ sink
+ case sink' of
+ Left _ -> error "sinkSubmission returned prematurely"
+ Right nSink -> modify $ Map.insert sId nSink
+ sinks <- execStateLC Map.empty . awaitForever $ \case
+ v@(Right (sId, _)) -> lift $ feed sId v
+ (Left f@File{..}) -> do
+ let
+ tryDecrypt :: FilePath -> _ (Either CryptoIDError SubmissionId)
+ tryDecrypt (CI.mk -> ciphertext) = try $ decrypt (CryptoID{..} :: CryptoFileNameSubmission)
+ acc (Just cID, fp) segment = return (Just cID, fp ++ [segment])
+ acc (Nothing , fp) segment = do
+ msId <- tryDecrypt segment
+ return . either (const id) (set _1 . Just) msId $ (Nothing, fp)
+ (msId, (joinPath -> fileTitle')) <- foldM acc (Nothing, []) $ splitDirectories fileTitle
+ lift . maybe (const $ return ()) feed msId $ Left f{ fileTitle = fileTitle' }
+ fmap Map.keysSet . lift $ mapM (void . closeResumableSink) sinks
diff --git a/src/Handler/Utils/Table.hs b/src/Handler/Utils/Table.hs
index bb1f621fd..478bd58ff 100644
--- a/src/Handler/Utils/Table.hs
+++ b/src/Handler/Utils/Table.hs
@@ -35,9 +35,6 @@ numberColonnade = headed "Nr" (fromString.show)
pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c
pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
-i18nCell :: RenderMessage site a => a -> Cell site
-i18nCell msg = cell [whamlet|_{msg}|]
-
-- Table Modification
encodeHeadedWidgetTableNumbered :: Attribute -> Colonnade Headed a (WidgetT site IO ()) -> [a] -> WidgetT site IO ()
@@ -94,3 +91,5 @@ headedRowSelector toExternal fromExternal attrs colonnade tdata = do
return ( catMaybes <$> collectResult selectionResults
, encodeCellTable attrs (pairColonnade selColonnade colonnade) (zip [0..] tdata)
)
+
+
diff --git a/src/Handler/Utils/Table/Pagination.hs b/src/Handler/Utils/Table/Pagination.hs
index 03b46992f..fad56aa03 100644
--- a/src/Handler/Utils/Table/Pagination.hs
+++ b/src/Handler/Utils/Table/Pagination.hs
@@ -1,6 +1,7 @@
{-# LANGUAGE NoImplicitPrelude
, ExistentialQuantification
, RecordWildCards
+ , NamedFieldPuns
, OverloadedStrings
, TemplateHaskell
, QuasiQuotes
@@ -10,29 +11,37 @@
, FlexibleInstances
, MultiParamTypeClasses
, TypeFamilies
+ , ScopedTypeVariables
+ , TupleSections
+ , RankNTypes
#-}
module Handler.Utils.Table.Pagination
( SortColumn(..), SortDirection(..)
, FilterColumn(..), IsFilterColumn
, DBRow(..), DBOutput
- , DBTable(..)
+ , DBTable(..), IsDBTable(..)
, PaginationSettings(..)
, PSValidator(..)
- , Sortable(..), sortable
+ , defaultFilter, defaultSorting
+ , restrictFilter, restrictSorting
+ , ToSortable(..), Sortable(..), sortable
, dbTable
+ , widgetColonnade, formColonnade
+ , textCell, stringCell, i18nCell, anchorCell
+ , formCell, DBFormResult, getDBFormResult
+ , dbRow, dbSelect
) where
import Handler.Utils.Table.Pagination.Types
-import Import
+import Import hiding (Proxy(..))
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect,unsafeSqlValue)
import qualified Database.Esqueleto.Internal.Language as E (From)
import Text.Blaze (Attribute)
import qualified Text.Blaze.Html5.Attributes as Html5
import qualified Text.Blaze.Html5 as Html5
-import Yesod.Core.Types (Body(..),GWData(..),WidgetT(..))
import qualified Data.Binary.Builder as Builder
@@ -42,6 +51,7 @@ import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Control.Monad.RWS hiding ((<>), Foldable(..), mapM_, forM_)
+import Control.Monad.Writer hiding ((<>), Foldable(..), mapM_, forM_)
import Data.Map (Map, (!))
import qualified Data.Map as Map
@@ -49,13 +59,17 @@ import qualified Data.Map as Map
import Data.Profunctor (lmap)
import Colonnade hiding (bool, fromMaybe, singleton)
+import qualified Colonnade (singleton)
import Colonnade.Encode
-import Yesod.Colonnade
import Text.Hamlet (hamletFile)
import Data.Ratio ((%))
+import Control.Lens
+
+import Data.Proxy
+
data SortColumn t = forall a. PersistField a => SortColumn { getSortColumn :: t -> E.SqlExpr (E.Value a) }
@@ -99,38 +113,38 @@ instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterColumn t cont, Mon
data DBRow r = DBRow
- { dbrIndex, dbrCount :: Int64
- , dbrOutput :: r
- }
+ { dbrOutput :: r
+ , dbrIndex, dbrCount :: Int64
+ } deriving (Show, Read, Eq, Ord)
class DBOutput r r' where
dbProj :: r -> r'
-instance DBOutput r r where
+instance DBOutput (DBRow r) (DBRow r) where
dbProj = id
instance DBOutput (DBRow r) r where
dbProj = dbrOutput
instance DBOutput (DBRow r) (Int64, r) where
dbProj = (,) <$> dbrIndex <*> dbrOutput
-
-data DBTable = forall a r r' h i t.
+data DBTable m x = forall a r r' h i t.
( ToSortable h, Functor h
, E.SqlSelect a r, DBOutput (DBRow r) r'
, PathPiece i
, E.From E.SqlQuery E.SqlExpr E.SqlBackend t
) => DBTable
{ dbtSQLQuery :: t -> E.SqlQuery a
- , dbtColonnade :: Colonnade h r' (Cell UniWorX)
- , dbtSorting :: Map Text (SortColumn t)
- , dbtFilter :: Map Text (FilterColumn t)
- , dbtAttrs :: Attribute
+ , dbtColonnade :: Colonnade h r' (DBCell m x)
+ , dbtSorting :: Map (CI Text) (SortColumn t)
+ , dbtFilter :: Map (CI Text) (FilterColumn t)
+ , dbtAttrs :: Attribute -- FIXME: currently unused
, dbtIdent :: i
}
+
data PaginationSettings = PaginationSettings
- { psSorting :: [(Text, SortDirection)]
- , psFilter :: Map Text [Text]
+ { psSorting :: [(CI Text, SortDirection)]
+ , psFilter :: Map (CI Text) [Text]
, psLimit :: Int64
, psPage :: Int64
, psShortcircuit :: Bool
@@ -145,9 +159,9 @@ instance Default PaginationSettings where
, psShortcircuit = False
}
-newtype PSValidator = PSValidator { runPSValidator :: DBTable -> Maybe PaginationSettings -> ([SomeMessage UniWorX], PaginationSettings) }
+newtype PSValidator m x = PSValidator { runPSValidator :: DBTable m x -> Maybe PaginationSettings -> ([SomeMessage UniWorX], PaginationSettings) }
-instance Default PSValidator where
+instance Default (PSValidator m x) where
def = PSValidator $ \DBTable{..} -> \case
Nothing -> def
Just ps -> swap . (\act -> execRWS act () ps) $ do
@@ -156,15 +170,94 @@ instance Default PSValidator where
modify $ \ps -> ps { psLimit = psLimit def }
tell . pure $ SomeMessage MsgPSLimitNonPositive
+defaultFilter :: Map (CI Text) [Text] -> PSValidator m x -> PSValidator m x
+defaultFilter psFilter (runPSValidator -> f) = PSValidator g
+ where
+ g dbTable Nothing = over _2 (\s -> s { psFilter }) $ f dbTable Nothing
+ g dbTable x = f dbTable x
-dbTable :: PSValidator -> DBTable -> Handler Widget
+defaultSorting :: [(CI Text, SortDirection)] -> PSValidator m x -> PSValidator m x
+defaultSorting psSorting (runPSValidator -> f) = PSValidator g
+ where
+ g dbTable Nothing = over _2 (\s -> s { psSorting }) $ f dbTable Nothing
+ g dbTable x = f dbTable x
+
+restrictFilter :: (CI Text -> [Text] -> Bool) -> PSValidator m x -> PSValidator m x
+restrictFilter restrict (runPSValidator -> f) = PSValidator $ \dbTable ps -> over _2 restrict' $ f dbTable ps
+ where
+ restrict' p = p { psFilter = Map.filterWithKey restrict $ psFilter p }
+
+restrictSorting :: (CI Text -> SortDirection -> Bool) -> PSValidator m x -> PSValidator m x
+restrictSorting restrict (runPSValidator -> f) = PSValidator $ \dbTable ps -> over _2 restrict' $ f dbTable ps
+ where
+ restrict' p = p { psSorting = filter (uncurry restrict) $ psSorting p }
+
+class (MonadHandler m, Monoid x) => IsDBTable (m :: * -> *) (x :: *) where
+ type DBResult m x :: *
+ -- type DBResult' m x :: *
+
+ data DBCell m x :: *
+ cellAttrs :: Lens' (DBCell m x) [(Text, Text)]
+ cellContents :: DBCell m x -> WriterT x m Widget
+
+ cell :: Widget -> DBCell m x
+
+
+ -- dbWidget :: Proxy m -> Proxy x -> Iso' (DBResult m x) (Widget, DBResult' m x)
+ dbWidget :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => Proxy m -> Proxy x -> DBResult m x -> m' Widget
+ runDBTable :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => m (x, Widget) -> m' (DBResult m x)
+
+instance IsDBTable (WidgetT UniWorX IO) () where
+ type DBResult (WidgetT UniWorX IO) () = Widget
+ -- type DBResult' (WidgetT UniWorX IO) () = ()
+
+ data DBCell (WidgetT UniWorX IO) () = WidgetCell
+ { dbCellAttrs :: [(Text, Text)]
+ , dbCellContents :: Widget
+ }
+ cellAttrs = lens dbCellAttrs $ \w as -> w { dbCellAttrs = as }
+ cellContents = return . dbCellContents
+
+ cell = WidgetCell []
+
+ -- dbWidget Proxy Proxy = iso (, ()) $ view _1
+ dbWidget Proxy Proxy = return
+ runDBTable = return . join . fmap (view _2)
+
+instance Monoid a => IsDBTable (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) where
+ -- type DBResult (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) = ((FormResult a, Widget), Enctype)
+ type DBResult (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) = Form a
+ -- type DBResult' (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) = (FormResult a, Enctype)
+
+ data DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) = FormCell
+ { formCellAttrs :: [(Text, Text)]
+ , formCellContents :: MForm (HandlerT UniWorX IO) (FormResult a, Widget)
+ }
+ cellAttrs = lens formCellAttrs $ \w as -> w { formCellAttrs = as }
+ cellContents = WriterT . fmap swap . formCellContents
+
+ cell widget = FormCell [] $ return (mempty, widget)
+
+ -- dbWidget Proxy Proxy = iso ((,) <$> view (_1._2) <*> ((,) <$> view (_1._1) <*> view _2))
+ -- ((,) <$> ((,) <$> view (_2._1) <*> view _1) <*> view (_2._2))
+ dbWidget Proxy Proxy = liftHandlerT . fmap (view $ _1 . _2) . runFormPost
+ -- runDBTable :: MForm (HandlerT UniWorX IO) (FormResult a, Widget) -> m ((FormResult a, Widget), Enctype)
+ -- runDBTable form = liftHandlerT . runFormPost $ \html -> over _2 (<> toWidget html) <$> form
+ -- runDBTable :: MForm (HandlerT UniWorX IO) (FormResult a, Widget) -> m (Html -> MForm (HandleT UniWorX IO) (FormResult a, Widget))
+ runDBTable = return . withFragment
+
+instance IsDBTable m a => IsString (DBCell m a) where
+ fromString = cell . fromString
+
+
+dbTable :: forall m x. IsDBTable m x => PSValidator m x -> DBTable m x -> Handler (DBResult m x)
dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent), dbtColonnade = (lmap dbProj -> dbtColonnade), .. }) = do
let
sortingOptions = mkOptionList
[ Option t' (t, d) t'
| (t, _) <- mapToList dbtSorting
, d <- [SortAsc, SortDesc]
- , let t' = t <> "-" <> toPathPiece d
+ , let t' = CI.foldedCase t <> "-" <> toPathPiece d
]
(_, defPS) = runPSValidator dbtable Nothing
wIdent n
@@ -181,7 +274,7 @@ dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent),
psResult <- runInputGetResult $ PaginationSettings
<$> (fromMaybe [] <$> iopt (multiSelectField $ return sortingOptions) (wIdent "sorting"))
- <*> (Map.mapMaybe ((\args -> args <$ guard (not $ null args)) =<<) <$> Map.traverseWithKey (\k _ -> iopt multiTextField $ wIdent k) dbtFilter)
+ <*> (Map.mapMaybe ((\args -> args <$ guard (not $ null args)) =<<) <$> Map.traverseWithKey (\k _ -> iopt multiTextField . wIdent $ CI.foldedCase k) dbtFilter)
<*> (fromMaybe (psLimit defPS) <$> iopt intField (wIdent "pagesize"))
<*> (fromMaybe (psPage defPS) <$> iopt intField (wIdent "page"))
<*> ireq checkBoxField (wIdent "table-only")
@@ -212,25 +305,44 @@ dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent),
rowCount
| ((_, E.Value n), _):_ <- rows' = n
| otherwise = 0
- rows = map (\((E.Value i, E.Value n), r) -> DBRow i n r) rows'
+ rows = map (\((E.Value dbrIndex, E.Value dbrCount), dbrOutput) -> DBRow{..}) rows'
- bool return (sendResponse <=< tblLayout) psShortcircuit $ do
- getParams <- handlerToWidget $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
- let table = $(widgetFile "table/colonnade")
- pageCount = max 1 . ceiling $ rowCount % psLimit
- pageNumbers = [0..pred pageCount]
+ table' :: WriterT x m Widget
+ table' = do
+ getParams <- liftHandlerT $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
+
+ let
tblLink f = decodeUtf8 . Builder.toLazyByteString . renderQueryText True $ f getParams
- withSortLinks Sortable{ sortableContent = Cell{..}, .. } = Cell
- { cellContents = $(widgetFile "table/sortable-header")
- , cellAttrs = maybe mempty (const sortableAttr) sortableKey <> cellAttrs
- }
- where
+ genHeaders SortableP{..} = forM (toSortable . oneColonnadeHead <$> getColonnade dbtColonnade) $ \Sortable{..} -> do
+ widget <- cellContents sortableContent
+ let
directions = [dir | (k, dir) <- psSorting, Just k == sortableKey ]
- sortableAttr = Html5.class_ . fromString . unwords $ "sortable" : foldMap toAttr directions
- toAttr SortAsc = ["sorted-asc"]
- toAttr SortDesc = ["sorted-desc"]
- $(widgetFile "table/layout")
+ isSortable = isJust sortableKey
+ isSorted = (`elem` directions)
+ attrs = sortableContent ^. cellAttrs
+ return $(widgetFile "table/cell/header")
+
+ columnCount :: Int64
+ columnCount = olength64 $ getColonnade dbtColonnade
+
+ wHeaders <- maybe (return Nothing) (fmap Just . genHeaders) pSortable
+
+ wRows <- forM rows $ \row -> forM (oneColonnadeEncode <$> getColonnade dbtColonnade) $ \(($ row) -> cell) -> do
+ widget <- cellContents cell
+ let attrs = cell ^. cellAttrs
+ return $(widgetFile "table/cell/body")
+
+ let table = $(widgetFile "table/colonnade")
+ pageCount = max 1 . ceiling $ rowCount % psLimit
+ pageNumbers = [0..pred pageCount]
+
+ return $(widgetFile "table/layout")
+
+ dbWidget' :: DBResult m x -> Handler Widget
+ dbWidget' = dbWidget (Proxy :: Proxy m) (Proxy :: Proxy x)
+
+ bool return (sendResponse <=< tblLayout <=< dbWidget') psShortcircuit <=< runDBTable . fmap swap $ runWriterT table'
where
tblLayout :: Widget -> Handler Html
tblLayout tbl' = do
@@ -240,22 +352,62 @@ dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent),
setParam :: Text -> Maybe Text -> QueryText -> QueryText
setParam key v qt = (key, v) : [ i | i@(key', _) <- qt, key' /= key ]
-widgetFromCell ::
- (Attribute -> WidgetT site IO () -> WidgetT site IO ())
- -> Cell site
- -> WidgetT site IO ()
-widgetFromCell f (Cell attrs contents) =
- f attrs contents
-td,th ::
- Attribute -> WidgetT site IO () -> WidgetT site IO ()
+--- DBCell utility functions
-td = liftParent Html5.td
-th = liftParent Html5.th
+widgetColonnade :: Headedness h
+ => Colonnade h r (DBCell (WidgetT UniWorX IO) ())
+ -> Colonnade h r (DBCell (WidgetT UniWorX IO) ())
+widgetColonnade = id
-liftParent :: (Html -> Html) -> Attribute -> WidgetT site IO a -> WidgetT site IO a
-liftParent el attrs (WidgetT f) = WidgetT $ \hdata -> do
- (a,gwd) <- f hdata
- let Body bodyFunc = gwdBody gwd
- newBodyFunc render =
- el Html5.! attrs $ (bodyFunc render)
- return (a,gwd { gwdBody = Body newBodyFunc })
+formColonnade :: (Headedness h, Monoid a)
+ => Colonnade h r (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a))
+ -> Colonnade h r (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a))
+formColonnade = id
+
+textCell, stringCell, i18nCell :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a
+stringCell = textCell
+i18nCell = textCell
+textCell msg = cell [whamlet|_{msg}|]
+
+anchorCell :: IsDBTable m a
+ => (r -> Route UniWorX)
+ -> (r -> Widget)
+ -> (r -> DBCell m a)
+anchorCell mkRoute mkWidget val = cell $(widgetFile "table/cell/link")
+ where
+ route = mkRoute val
+ widget = mkWidget val
+
+newtype DBFormResult r i a = DBFormResult (Map i (r, a -> a))
+
+instance Ord i => Monoid (DBFormResult r i a) where
+ mempty = DBFormResult Map.empty
+ (DBFormResult m1) `mappend` (DBFormResult m2) = DBFormResult $ Map.unionWith (\(r, f1) (_, f2) -> (r, f2 . f1)) m1 m2
+
+getDBFormResult :: forall r i a. Ord i => (r -> a) -> DBFormResult r i a -> Map i a
+getDBFormResult initial (DBFormResult m) = Map.map (\(r, f) -> f $ initial r) m
+
+formCell :: forall r i a. Ord i
+ => (r -> MForm (HandlerT UniWorX IO) i)
+ -> (r -> i -> MForm (HandlerT UniWorX IO) (FormResult (a -> a), Widget))
+ -> (r -> DBCell ((RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO))) (FormResult (DBFormResult r i a)))
+formCell genIndex genForm input = FormCell
+ { formCellAttrs = []
+ , formCellContents = do -- MForm (HandlerT UniWorX IO) (FormResult (Map i (Endo a)), Widget)
+ i <- genIndex input
+ (edit, w) <- genForm input i
+ return (DBFormResult . Map.singleton i . (input,) <$> edit, w)
+ }
+
+-- Predefined colonnades
+
+dbRow :: forall h r m a. (Headedness h, IsDBTable m a) => Colonnade h (DBRow r) (DBCell m a)
+dbRow = Colonnade.singleton (headednessPure $ textCell MsgNrColumn) $ \DBRow{ dbrIndex } -> textCell $ tshow dbrIndex
+
+dbSelect :: forall h r i a. (Headedness h, Ord i, PathPiece i)
+ => Setter' a Bool
+ -> (r -> MForm (HandlerT UniWorX IO) i)
+ -> Colonnade h r (DBCell ((RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO))) (FormResult (DBFormResult r i a)))
+dbSelect resLens genIndex = Colonnade.singleton (headednessPure $ textCell MsgSelectColumn) $ \r -> flip (formCell genIndex) r $ \_ i -> do
+ (selResult, selWidget) <- mreq checkBoxField ("" { fsName = Just $ "select-" <> toPathPiece i }) (Just False)
+ return (set resLens <$> selResult, [whamlet|^{fvInput selWidget}|])
diff --git a/src/Handler/Utils/Table/Pagination/Types.hs b/src/Handler/Utils/Table/Pagination/Types.hs
index 1c0c883d6..0a0b6c1c2 100644
--- a/src/Handler/Utils/Table/Pagination/Types.hs
+++ b/src/Handler/Utils/Table/Pagination/Types.hs
@@ -11,12 +11,14 @@ import Import hiding (singleton)
import Colonnade
import Colonnade.Encode
+import Data.CaseInsensitive (CI)
+
data Sortable a = Sortable
- { sortableKey :: Maybe Text
+ { sortableKey :: Maybe (CI Text)
, sortableContent :: a
}
-sortable :: Maybe Text -> c -> (a -> c) -> Colonnade Sortable a c
+sortable :: Maybe (CI Text) -> c -> (a -> c) -> Colonnade Sortable a c
sortable k h = singleton (Sortable k h)
instance Headedness Sortable where
@@ -40,4 +42,3 @@ instance ToSortable Headed where
instance ToSortable Headless where
pSortable = Nothing
-
diff --git a/src/Model/Types.hs b/src/Model/Types.hs
index f054899b2..efa329e4a 100644
--- a/src/Model/Types.hs
+++ b/src/Model/Types.hs
@@ -109,7 +109,7 @@ data Load -- = ByTutorial { countsToLoad :: Bool } | ByProportion { load :: Rati
= Load { byTutorial :: Maybe Bool -- ^ Just all from Tutorial, True if counting towards overall workload
, byProportion :: Rational -- ^ workload proportion of all submission not assigned to tutorial leaders
}
- deriving (Show, Read, Eq)
+ deriving (Show, Read, Eq, Ord)
derivePersistField "Load"
instance Semigroup Load where
@@ -169,14 +169,30 @@ termFromText t
, Right season <- seasonFromChar s
= Right TermIdentifier{..}
| otherwise = Left $ "Invalid TermIdentifier: “" <> t <> "”"
+
+termToRational :: TermIdentifier -> Rational
+termToRational TermIdentifier{..} = fromInteger year + seasonOffset
+ where
+ seasonOffset
+ | Summer <- season = 0
+ | Winter <- season = 0.5
+
+termFromRational :: Rational -> TermIdentifier
+termFromRational n = TermIdentifier{..}
+ where
+ year = floor n
+ remainder = n - (fromInteger $ floor n)
+ season
+ | remainder == 0 = Summer
+ | otherwise = Winter
instance PersistField TermIdentifier where
- toPersistValue = PersistText . termToText
- fromPersistValue (PersistText t) = termFromText t
+ toPersistValue = PersistRational . termToRational
+ fromPersistValue (PersistRational t) = Right $ termFromRational t
fromPersistValue x = Left $ "Expected TermIdentifier, received: " <> tshow x
instance PersistFieldSql TermIdentifier where
- sqlType _ = SqlString
+ sqlType _ = SqlNumeric 5 1
instance ToHttpApiData TermIdentifier where
toUrlPiece = termToText
diff --git a/src/Utils.hs b/src/Utils.hs
index 989e3cda4..a6b5d02ab 100644
--- a/src/Utils.hs
+++ b/src/Utils.hs
@@ -43,6 +43,10 @@ getMsgRenderer = do
mr <- getMessageRender
return $ MsgRenderer (mr . SomeMessage :: forall msg. RenderMessage site msg => msg -> Text)
+instance Monad FormResult where
+ FormMissing >>= _ = FormMissing
+ (FormFailure errs) >>= _ = FormFailure errs
+ (FormSuccess a) >>= f = f a
---------------------
-- Text and String --
diff --git a/src/Utils/Common.hs b/src/Utils/Common.hs
index 3b0d537b8..8583ccf86 100644
--- a/src/Utils/Common.hs
+++ b/src/Utils/Common.hs
@@ -16,7 +16,7 @@ import Language.Haskell.TH
-- Tuples --
------------
-
+-- Alternatively uses lenses: "^. _3" projects the 3rd component of an n-tuple for any n >=3, requires import Control.Lens
projNI :: Int -> Int -> ExpQ -- generic projection gives I-th element of N-tuple, i.e. snd3 = $(projNI 3 2) --ghci -fth
-- $(projN n m) :: (t1,..,tn) -> tm (for m<=n)
projNI n i = lamE [pat] rhs
diff --git a/templates/adminTest.hamlet b/templates/adminTest.hamlet
index 66e2b891c..d18c0b156 100644
--- a/templates/adminTest.hamlet
+++ b/templates/adminTest.hamlet
@@ -36,10 +36,7 @@
Neues Semester anlegen
- Kurse anlegen
-
-
- Dateien hochladen und abrufen
+ Kurse anlegen
diff --git a/templates/corrections.hamlet b/templates/corrections.hamlet
new file mode 100644
index 000000000..766cda831
--- /dev/null
+++ b/templates/corrections.hamlet
@@ -0,0 +1,5 @@
+