Merge branch 'fix/submission' into 'live'

Uncontroversial changes fixing submission uploads

Closes #128 and #130

See merge request !63
This commit is contained in:
Gregor Kleen 2018-07-23 13:16:42 +02:00
commit eafba24c30
13 changed files with 173 additions and 34 deletions

View File

@ -0,0 +1,12 @@
$# Syntax:
$# - Leere zeilen werden ignoriert
$# - Zeilen, die mit '$#' beginnen, werden ignoriert
$# - Verbleibende Zeilen werden jeweils als `Glob`-Pattern kompiliert
$# Ignoriere rekursiv alle Ordner __MACOSX und ihren Inhalt
**/__MACOSX
**/__MACOSX/*
**/__MACOSX/**/*
$# Ignoriere rekursiv alle Dateien .DS_Store
**/.DS_Store

View File

@ -174,6 +174,7 @@ UpdatedAssignedCorrectorsAuto num@Int64: #{display num} Abgaben wurden unter den
CouldNotAssignCorrectorsAuto num@Int64: #{display num} Abgaben konnten nicht automatisch zugewiesen werden: CouldNotAssignCorrectorsAuto num@Int64: #{display num} Abgaben konnten nicht automatisch zugewiesen werden:
CorrectionsUploaded num@Int64: #{display num} Korrekturen wurden gespeichert: CorrectionsUploaded num@Int64: #{display num} Korrekturen wurden gespeichert:
NoCorrectionsUploaded: In der hochgeladenen Datei wurden keine Korrekturen gefunden.
RatingBy: Korrigiert von RatingBy: Korrigiert von
AchievedBonusPoints: Erreichte Bonuspunkte AchievedBonusPoints: Erreichte Bonuspunkte
@ -221,3 +222,6 @@ AmbiguousUTCTime: Der angegebene Zeitpunkt lässt sich nicht eindeutig zu UTC ko
LastEdits: Letzte Änderungen LastEdits: Letzte Änderungen
EditedBy name@Text time@Text: Durch #{name} um #{time} EditedBy name@Text time@Text: Durch #{name} um #{time}
LastEdit: Letzte Änderung LastEdit: Letzte Änderung
SubmissionFilesIgnored: Es wurden Dateien in der hochgeladenen Abgabe ignoriert:
SubmissionDoesNotExist smid@CryptoFileNameSubmission: Es existiert keine Abgabe mit Nummer #{toPathPiece smid}.

View File

@ -87,6 +87,7 @@ dependencies:
- system-locale - system-locale
- th-lift-instances - th-lift-instances
- gitrev - gitrev
- Glob
# 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.

View File

@ -872,12 +872,7 @@ pageActions (CSubmissionR tid csh shn cid SubShowR) =
{ menuItemLabel = "Korrektur" { menuItemLabel = "Korrektur"
, menuItemIcon = Nothing , menuItemIcon = Nothing
, menuItemRoute = CSubmissionR tid csh shn cid CorrectionR , menuItemRoute = CSubmissionR tid csh shn cid CorrectionR
, menuItemAccessCallback' = do , menuItemAccessCallback' = return True
smid <- decrypt cid
sm <- runDB $ get smid
case sm of
(Just (Submission { submissionRatingTime=Just _})) -> return True
_ -> return False
} }
] ]
pageActions (CSheetR tid csh shn SCorrR) = pageActions (CSheetR tid csh shn SCorrR) =

View File

@ -14,6 +14,7 @@
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE MultiWayIf #-}
module Handler.Corrections where module Handler.Corrections where
@ -215,7 +216,7 @@ correctionsR whereClause (formColonnade -> displayColumns) psValidator actions =
when (not $ null alreadyAssigned) $ do when (not $ null alreadyAssigned) $ do
mr <- (toHtml . ) <$> getMessageRender mr <- (toHtml . ) <$> getMessageRender
alreadyAssigned' <- forM alreadyAssigned $ \Entity{..} -> (, entityVal) <$> (encrypt entityKey :: DB CryptoFileNameSubmission) alreadyAssigned' <- forM alreadyAssigned $ \Entity{..} -> (, entityVal) <$> (encrypt entityKey :: DB CryptoFileNameSubmission)
addMessage "warn" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsAlreadyAssigned.hamlet") mr) addMessage "warning" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsAlreadyAssigned.hamlet") mr)
let unassigned = Set.fromList subs `Set.difference` Set.fromList (entityKey <$> alreadyAssigned) let unassigned = Set.fromList subs `Set.difference` Set.fromList (entityKey <$> alreadyAssigned)
when (not $ null unassigned) $ do when (not $ null unassigned) $ do
num <- updateWhereCount [SubmissionId <-. Set.toList unassigned] [SubmissionRatingBy =. Just uid] num <- updateWhereCount [SubmissionId <-. Set.toList unassigned] [SubmissionRatingBy =. Just uid]
@ -238,7 +239,7 @@ correctionsR whereClause (formColonnade -> displayColumns) psValidator actions =
when (not $ null alreadyAssigned) $ do when (not $ null alreadyAssigned) $ do
mr <- (toHtml . ) <$> getMessageRender mr <- (toHtml . ) <$> getMessageRender
alreadyAssigned' <- forM alreadyAssigned $ \Entity{..} -> (, entityVal) <$> (encrypt entityKey :: DB CryptoFileNameSubmission) alreadyAssigned' <- forM alreadyAssigned $ \Entity{..} -> (, entityVal) <$> (encrypt entityKey :: DB CryptoFileNameSubmission)
addMessage "warn" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsAlreadyAssigned.hamlet") mr) addMessage "warning" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsAlreadyAssigned.hamlet") mr)
let unassigned = Set.fromList subs `Set.difference` Set.fromList (entityKey <$> alreadyAssigned) let unassigned = Set.fromList subs `Set.difference` Set.fromList (entityKey <$> alreadyAssigned)
when (not $ null unassigned) $ do when (not $ null unassigned) $ do
(assigned, unassigned) <- assignSubmissions shid (Just unassigned) (assigned, unassigned) <- assignSubmissions shid (Just unassigned)
@ -247,7 +248,7 @@ correctionsR whereClause (formColonnade -> displayColumns) psValidator actions =
when (not $ null unassigned) $ do when (not $ null unassigned) $ do
mr <- (toHtml . ) <$> getMessageRender mr <- (toHtml . ) <$> getMessageRender
unassigned' <- forM (Set.toList unassigned) $ \sid -> (encrypt sid :: DB CryptoFileNameSubmission) unassigned' <- forM (Set.toList unassigned) $ \sid -> (encrypt sid :: DB CryptoFileNameSubmission)
addMessage "warn" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsNotAssignedAuto.hamlet") mr) addMessage "warning" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsNotAssignedAuto.hamlet") mr)
redirect currentRoute redirect currentRoute
fmap toTypedContent . defaultLayout $ do fmap toTypedContent . defaultLayout $ do
@ -403,7 +404,7 @@ postCorrectionR tid csh shn cid = do
FormSuccess fileSource -> do FormSuccess fileSource -> do
uid <- requireAuthId uid <- requireAuthId
runDB . runConduit $ transPipe lift fileSource .| extractRatings .| sinkSubmission uid (Right sub) True runDB . runConduit $ transPipe lift fileSource .| extractRatingsMsg .| sinkSubmission uid (Right sub) True
addMessageI "success" MsgRatingFilesUpdated addMessageI "success" MsgRatingFilesUpdated
redirect $ CSubmissionR tid csh shn cid CorrectionR redirect $ CSubmissionR tid csh shn cid CorrectionR
@ -438,10 +439,14 @@ postCorrectionsUploadR = do
FormFailure errs -> mapM_ (addMessage "error" . toHtml) errs FormFailure errs -> mapM_ (addMessage "error" . toHtml) errs
FormSuccess files -> do FormSuccess files -> do
uid <- requireAuthId uid <- requireAuthId
subs <- runDB . runConduit $ transPipe lift files .| extractRatings .| sinkMultiSubmission uid True subs <- runDB . runConduit $ transPipe lift files .| extractRatingsMsg .| sinkMultiSubmission uid True
subs' <- traverse encrypt $ Set.toList subs :: Handler [CryptoFileNameSubmission] if
mr <- (toHtml .) <$> getMessageRender | null subs -> addMessageI "warning" MsgNoCorrectionsUploaded
addMessage "success" =<< withUrlRenderer ($(ihamletFile "templates/messages/correctionsUploaded.hamlet") mr) | otherwise -> do
subs' <- traverse encrypt $ Set.toList subs :: Handler [CryptoFileNameSubmission]
mr <- (toHtml .) <$> getMessageRender
addMessage "success" =<< withUrlRenderer ($(ihamletFile "templates/messages/correctionsUploaded.hamlet") mr)
defaultLayout $ do defaultLayout $ do
$(widgetFile "corrections-upload") $(widgetFile "corrections-upload")

View File

@ -495,7 +495,7 @@ correctorForm shid = do
(defaultLoads', currentLoads') <- lift . runDB $ (,) <$> defaultLoads shid <*> currentLoads (defaultLoads', currentLoads') <- lift . runDB $ (,) <$> defaultLoads shid <*> currentLoads
loads' <- fmap (Map.fromList [(uid, mempty) | uid <- formCIDs] `Map.union`) $ if loads' <- fmap (Map.fromList [(uid, mempty) | uid <- formCIDs] `Map.union`) $ if
| Map.null currentLoads' | Map.null currentLoads'
, null formCIDs -> defaultLoads' <$ when (not $ Map.null defaultLoads') (addMessageI "warn" MsgCorrectorsDefaulted) , null formCIDs -> defaultLoads' <$ when (not $ Map.null defaultLoads') (addMessageI "warning" MsgCorrectorsDefaulted)
| otherwise -> return $ Map.fromList (map (, mempty) formCIDs) `Map.union` currentLoads' | otherwise -> return $ Map.fromList (map (, mempty) formCIDs) `Map.union` currentLoads'
deletions <- lift $ foldM (\dels uid -> maybe (Set.insert uid dels) (const dels) <$> guardNonDeleted uid) Set.empty (Map.keys loads') deletions <- lift $ foldM (\dels uid -> maybe (Set.insert uid dels) (const dels) <$> guardNonDeleted uid) Set.empty (Map.keys loads')

View File

@ -220,7 +220,7 @@ submissionHelper tid csh shn (SubmissionMode mcid) = do
(Nothing, Just smid) -- no new files, existing submission partners updated (Nothing, Just smid) -- no new files, existing submission partners updated
-> return smid -> return smid
(Just files, _) -- new files (Just files, _) -- new files
-> runConduit $ transPipe lift files .| extractRatings .| sinkSubmission uid (maybe (Left shid) Right msmid) False -> runConduit $ transPipe lift files .| extractRatingsMsg .| sinkSubmission uid (maybe (Left shid) Right msmid) False
_ -> error "Impossible, because of definition of `makeSubmissionForm`" _ -> error "Impossible, because of definition of `makeSubmissionForm`"
-- Determine members of pre-registered group -- Determine members of pre-registered group
groupUids <- fmap (setFromList . map E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser') -> do groupUids <- fmap (setFromList . map E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser') -> do

View File

@ -17,7 +17,7 @@ import Handler.Utils.Table as Handler.Utils
import Handler.Utils.Table.Pagination as Handler.Utils import Handler.Utils.Table.Pagination as Handler.Utils
import Handler.Utils.Zip as Handler.Utils import Handler.Utils.Zip as Handler.Utils
import Handler.Utils.Rating as Handler.Utils import Handler.Utils.Rating as Handler.Utils hiding (extractRatings)
import Handler.Utils.Submission as Handler.Utils import Handler.Utils.Submission as Handler.Utils
import Handler.Utils.Sheet as Handler.Utils import Handler.Utils.Sheet as Handler.Utils
import Handler.Utils.Templates as Handler.Utils import Handler.Utils.Templates as Handler.Utils

View File

@ -15,6 +15,7 @@
module Handler.Utils.Submission module Handler.Utils.Submission
( AssignSubmissionException(..) ( AssignSubmissionException(..)
, assignSubmissions , assignSubmissions
, submissionBlacklist, filterSubmission, extractRatings, extractRatingsMsg
, submissionFileSource, submissionFileQuery , submissionFileSource, submissionFileQuery
, submissionMultiArchive , submissionMultiArchive
, SubmissionSinkException(..) , SubmissionSinkException(..)
@ -23,12 +24,15 @@ module Handler.Utils.Submission
) where ) where
import Import hiding ((.=), joinPath) import Import hiding ((.=), joinPath)
import Yesod.Core.Types (HandlerContents(..), ErrorResponse(..))
import Control.Lens import Control.Lens
import Control.Lens.Extras (is) import Control.Lens.Extras (is)
import Utils.Lens import Utils.Lens
import Control.Monad.State hiding (forM_, mapM_,foldM) import Control.Monad.State hiding (forM_, mapM_,foldM)
import Control.Monad.Writer (MonadWriter(..))
import Control.Monad.RWS.Lazy (RWST)
import qualified Control.Monad.Random as Rand import qualified Control.Monad.Random as Rand
import Data.Maybe import Data.Maybe
@ -45,16 +49,24 @@ import qualified Data.CaseInsensitive as CI
import Data.Monoid (Monoid, Any(..)) import Data.Monoid (Monoid, Any(..))
import Generics.Deriving.Monoid (memptydefault, mappenddefault) import Generics.Deriving.Monoid (memptydefault, mappenddefault)
import Handler.Utils.Rating import Handler.Utils.Rating hiding (extractRatings)
import qualified Handler.Utils.Rating as Rating (extractRatings)
import Handler.Utils.Zip import Handler.Utils.Zip
import Handler.Utils.Sheet import Handler.Utils.Sheet
import Handler.Utils.Submission.TH
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import Data.Conduit
import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.List as Conduit
import Data.Conduit.ResumableSink import Data.Conduit.ResumableSink
import System.FilePath import System.FilePath
import System.FilePath.Glob
import Text.Hamlet (ihamletFile)
import qualified Control.Monad.Catch as E (Handler(..))
data AssignSubmissionException = NoCorrectorsByProportion data AssignSubmissionException = NoCorrectorsByProportion
@ -191,6 +203,42 @@ data SubmissionSinkException = DuplicateFileTitle FilePath
instance Exception SubmissionSinkException instance Exception SubmissionSinkException
submissionBlacklist :: [Pattern]
submissionBlacklist = $(patternFile compDefault "config/submission-blacklist")
filterSubmission :: MonadLogger m => ConduitM File File m (Set FilePath)
-- ^ Removes all `File`s matching `submissionBlacklist`, returning their `fileTitle`s
filterSubmission = do
$logDebugS "filterSubmission" $ tshow submissionBlacklist
execWriterLC . awaitForever $ \case
File{fileTitle}
| any (`match'` fileTitle) submissionBlacklist -> tell $ Set.singleton fileTitle
file -> yield file
where
match' = matchWith $ matchDefault
{ matchDotsImplicitly = True -- Special treatment for . makes no sense since we're multiplatform
}
extractRatings :: ( MonadHandler m
, HandlerSite m ~ UniWorX
, MonadCatch m
, MonadLogger m
) => ConduitM File SubmissionContent m (Set FilePath)
extractRatings = filterSubmission `fuseUpstream` Rating.extractRatings
extractRatingsMsg :: ( MonadHandler m
, HandlerSite m ~ UniWorX
, MonadCatch m
, MonadLogger m
) => Conduit File m SubmissionContent
extractRatingsMsg = do
ignored' <- filterSubmission `fuseUpstream` Rating.extractRatings
let ignored :: Set (Either CryptoFileNameSubmission FilePath)
ignored = Right `Set.map` ignored'
unless (null ignored) $ do
mr <- (toHtml . ) <$> getMessageRender
addMessage "warning" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionFilesIgnored.hamlet") mr)
sinkSubmission :: UserId sinkSubmission :: UserId
-> Either SheetId SubmissionId -> Either SheetId SubmissionId
-> Bool -- ^ Is this a correction -> Bool -- ^ Is this a correction
@ -228,7 +276,7 @@ sinkSubmission userId mExists isUpdate = do
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) $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 }
@ -398,7 +446,9 @@ sinkMultiSubmission userId isUpdate = do
let let
feed :: SubmissionId feed :: SubmissionId
-> SubmissionContent -> SubmissionContent
-> StateT -> RWST
()
_
(Map SubmissionId (ResumableSink SubmissionContent (YesodDB UniWorX) SubmissionId)) (Map SubmissionId (ResumableSink SubmissionContent (YesodDB UniWorX) SubmissionId))
(YesodDB UniWorX) (YesodDB UniWorX)
() ()
@ -408,8 +458,9 @@ sinkMultiSubmission userId isUpdate = do
Just sink -> return sink Just sink -> return sink
Nothing -> do Nothing -> do
lift $ do lift $ do
Submission{..} <- get404 sId
cID <- encrypt sId cID <- encrypt sId
$(logDebugS) "sinkMultiSubmission" $ "Doing auth checks for " <> toPathPiece cID
Submission{..} <- get404 sId
Sheet{..} <- get404 submissionSheet Sheet{..} <- get404 submissionSheet
Course{..} <- get404 sheetCourse Course{..} <- get404 sheetCourse
authRes <- evalAccessDB (CSubmissionR courseTerm courseShorthand sheetName cID CorrectionR) True authRes <- evalAccessDB (CSubmissionR courseTerm courseShorthand sheetName cID CorrectionR) True
@ -422,30 +473,45 @@ sinkMultiSubmission userId isUpdate = do
case sink' of case sink' of
Left _ -> error "sinkSubmission returned prematurely" Left _ -> error "sinkSubmission returned prematurely"
Right nSink -> modify $ Map.insert sId nSink Right nSink -> modify $ Map.insert sId nSink
sinks <- execStateLC Map.empty . awaitForever $ \case (sinks, ignored) <- execRWSLC () Map.empty . awaitForever $ \case
v@(Right (sId, _)) -> do v@(Right (sId, _)) -> do
cID <- encrypt sId cID <- encrypt sId
handle (throwM . SubmissionSinkException cID Nothing) $ $logDebugS "sinkMultiSubmission" $ "Feeding rating for " <> toPathPiece cID
lift $ feed sId v lift (feed sId v) `catches` [ E.Handler (throwM . SubmissionSinkException cID Nothing), E.Handler (void . handleHCError (Left cID)) ]
(Left f@File{..}) -> do (Left f@File{..}) -> do
let let
tryDecrypt :: FilePath -> _ (Either SomeException SubmissionId) acc :: (Maybe SubmissionId, [FilePath]) -> FilePath -> _ (Maybe SubmissionId, [FilePath])
tryDecrypt (CI.mk -> ciphertext) = try $ decrypt (CryptoID{..} :: CryptoFileNameSubmission) acc (Just sId, fp) segment = return (Just sId, fp ++ [segment])
acc (Just cID, fp) segment = return (Just cID, fp ++ [segment])
acc (Nothing , fp) segment = do acc (Nothing , fp) segment = do
msId <- tryDecrypt segment let
return . either (const id) (set _1 . Just) msId $ (Nothing, fp) tryDecrypt ciphertext = do
sId <- decrypt (CryptoID (CI.mk segment) :: CryptoFileNameSubmission)
Just sId <$ get404 sId
msId <- lift (lift $ tryDecrypt segment) `catches` [ E.Handler handleCryptoID, E.Handler (handleHCError $ Right fileTitle) ]
return (msId, fp)
(msId, (joinPath -> fileTitle')) <- foldM acc (Nothing, []) $ splitDirectories fileTitle (msId, (joinPath -> fileTitle')) <- foldM acc (Nothing, []) $ splitDirectories fileTitle
$logDebugS "sinkMultiSubmission" $ tshow (splitDirectories fileTitle, msId, fileTitle')
case msId of case msId of
Nothing -> $logDebugS "sinkMultiSubmission" "Dropped" Nothing -> do
$logDebugS "sinkMultiSubmission" $ "Dropping " <> tshow (splitDirectories fileTitle, msId, fileTitle')
Just sId -> do Just sId -> do
$logDebugS "sinkMultiSubmission" $ "Feeding " <> tshow (splitDirectories fileTitle, msId, fileTitle')
cID <- encrypt sId cID <- encrypt sId
handle (throwM . SubmissionSinkException cID (Just fileTitle)) $ handle (throwM . SubmissionSinkException cID (Just fileTitle)) $
lift . feed sId $ Left f{ fileTitle = fileTitle' } lift . feed sId $ Left f{ fileTitle = fileTitle' }
when (not $ null ignored) $ do
mr <- (toHtml .) <$> getMessageRender
addMessage "warning" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionFilesIgnored.hamlet") mr)
fmap Map.keysSet . lift . sequence $ flip Map.mapWithKey sinks $ \sId sink -> do fmap Map.keysSet . lift . sequence $ flip Map.mapWithKey sinks $ \sId sink -> do
cID <- encrypt sId cID <- encrypt sId
handle (throwM . SubmissionSinkException cID Nothing) . void $ closeResumableSink sink handle (throwM . SubmissionSinkException cID Nothing) $
void $ closeResumableSink sink
where
handleHCError :: Either CryptoFileNameSubmission FilePath -> HandlerContents -> _ (Maybe a)
handleHCError ident (HCError NotFound) = Nothing <$ tell (Set.singleton ident)
handleHCError _ e = throwM e
handleCryptoID :: CryptoIDError -> _ (Maybe a)
handleCryptoID _ = return Nothing
submissionMatchesSheet :: TermId -> Text -> Text -> CryptoFileNameSubmission -> DB () submissionMatchesSheet :: TermId -> Text -> Text -> CryptoFileNameSubmission -> DB ()
submissionMatchesSheet tid csh shn cid = do submissionMatchesSheet tid csh shn cid = do

View File

@ -0,0 +1,39 @@
{-# LANGUAGE NoImplicitPrelude
, TemplateHaskell
, ViewPatterns
, OverloadedStrings
, StandaloneDeriving
, DeriveLift
#-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Utils.Submission.TH
( patternFile
) where
import ClassyPrelude.Yesod
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (qAddDependentFile, Lift(..))
import System.FilePath.Glob
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
deriving instance Lift CompOptions
-- $(patternFile compDefault file) :: [System.FilePath.Glob.Pattern]
patternFile :: CompOptions -> FilePath -> ExpQ
patternFile opts file = do
qAddDependentFile file
patternStrings <- runIO $ filter (not . isComment) . Text.lines <$> Text.readFile file
listE $ map (\(Text.unpack -> pat) -> [|compileWith opts pat|]) patternStrings
isComment :: Text -> Bool
isComment line = or
[ commentSymbol `Text.isPrefixOf` Text.stripStart line
, Text.null $ Text.strip line
]
where
commentSymbol = "$#"

View File

@ -38,7 +38,7 @@ extra-deps:
- uuid-crypto-1.4.0.0 - uuid-crypto-1.4.0.0
- filepath-crypto-0.1.0.0 - filepath-crypto-0.1.0.0
- cryptoids-0.5.0.0 - cryptoids-0.5.1.0
- cryptoids-types-0.0.0 - cryptoids-types-0.0.0
- cryptoids-class-0.0.0 - cryptoids-class-0.0.0

View File

@ -171,6 +171,8 @@ h4 {
background-color: white; background-color: white;
overflow: hidden; overflow: hidden;
transition: padding-left .2s ease-out; transition: padding-left .2s ease-out;
max-width: 1200px;
margin: 0 auto;
> .container { > .container {
margin: 20px 0; margin: 20px 0;
@ -185,6 +187,13 @@ h4 {
} }
} }
.logged-in {
.main__content {
margin: 0;
max-width: none;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.logged-in { .logged-in {
.main__content { .main__content {
@ -210,7 +219,6 @@ h4 {
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
.logged-in { .logged-in {
.main__content { .main__content {
padding-left: 320px; padding-left: 320px;

View File

@ -0,0 +1,9 @@
_{MsgSubmissionFilesIgnored}
<ul>
$forall ident <- ignored
$case ident
$of Right fileTitle
<li style="font-family: monospace">#{fileTitle}
$of Left cID
<li>Bewertungsdatei für <span style="font-family: monospace">#{toPathPiece cID}</span>