Merge branch 'master' into 'live'

#225

Closes #225

See merge request !90
This commit is contained in:
Gregor Kleen 2018-11-03 22:59:17 +01:00
commit d654310120
79 changed files with 1222 additions and 1199 deletions

14
.hlint.yaml Normal file
View File

@ -0,0 +1,14 @@
# HLint configuration file
# https://github.com/ndmitchell/hlint
##########################
- ignore: { name: "Parse error" }
- ignore: { name: "Reduce duplication" }
- ignore: { name: "Use ||" }
- ignore: { name: "Use &&" }
- ignore: { name: "Use ++" }
- arguments:
- -XQuasiQuotes
- -XTemplateHaskell
- -j

4
hlint/Hlint.hs Normal file
View File

@ -0,0 +1,4 @@
{-# OPTIONS_GHC
-F -pgmF hlint-test
-optF src
#-}

4
models
View File

@ -11,7 +11,7 @@ User json
dateFormat DateTimeFormat "default='%d.%m.%Y'" dateFormat DateTimeFormat "default='%d.%m.%Y'"
timeFormat DateTimeFormat "default='%R'" timeFormat DateTimeFormat "default='%R'"
downloadFiles Bool default=false downloadFiles Bool default=false
mailLanguages MailLanguages "default='[]'" mailLanguages MailLanguages default='[]'
notificationSettings NotificationSettings notificationSettings NotificationSettings
UniqueAuthentication ident UniqueAuthentication ident
UniqueEmail email UniqueEmail email
@ -139,7 +139,7 @@ File
title FilePath title FilePath
content ByteString Maybe -- Nothing iff this is a directory content ByteString Maybe -- Nothing iff this is a directory
modified UTCTime modified UTCTime
deriving Show Eq deriving Show Eq Generic
Submission Submission
sheet SheetId sheet SheetId
ratingPoints Points Maybe -- "Just" does not mean done ratingPoints Points Maybe -- "Just" does not mean done

View File

@ -2,114 +2,111 @@ name: uniworx
version: "0.0.0" version: "0.0.0"
dependencies: dependencies:
# Due to a bug in GHC 8.0.1, we block its usage
# Due to a bug in GHC 8.0.1, we block its usage # See: https://ghc.haskell.org/trac/ghc/ticket/12130
# See: https://ghc.haskell.org/trac/ghc/ticket/12130 - base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5
- base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5 # version 1.0 had a bug in reexporting Handler, causing trouble
- classy-prelude-yesod >=0.10.2 && <1.0 || >=1.1
# version 1.0 had a bug in reexporting Handler, causing trouble - foreign-store
- classy-prelude-yesod >=0.10.2 && <1.0 || >=1.1 - yesod >=1.4.3 && <1.5
- yesod-core >=1.4.30 && <1.5
- foreign-store - yesod-auth >=1.4.0 && <1.5
- yesod >=1.4.3 && <1.5 - yesod-static >=1.4.0.3 && <1.6
- yesod-core >=1.4.30 && <1.5 - yesod-form >=1.4.0 && <1.5
- yesod-auth >=1.4.0 && <1.5 - classy-prelude >=0.10.2
- yesod-static >=1.4.0.3 && <1.6 - classy-prelude-conduit >=0.10.2
- yesod-form >=1.4.0 && <1.5 - bytestring >=0.9 && <0.11
- classy-prelude >=0.10.2 - text >=0.11 && <2.0
- classy-prelude-conduit >=0.10.2 - persistent >=2.7.2 && <2.8
- bytestring >=0.9 && <0.11 - persistent-postgresql >=2.1.1 && <2.8
- text >=0.11 && <2.0 - persistent-template >=2.0 && <2.8
- persistent >=2.7.2 && <2.8 - template-haskell
- persistent-postgresql >=2.1.1 && <2.8 - shakespeare >=2.0 && <2.1
- persistent-template >=2.0 && <2.8 - hjsmin >=0.1 && <0.3
- template-haskell - monad-control >=0.3 && <1.1
- shakespeare >=2.0 && <2.1 - wai-extra >=3.0 && <3.1
- hjsmin >=0.1 && <0.3 - yaml >=0.8 && <0.9
- monad-control >=0.3 && <1.1 - http-conduit >=2.1 && <2.3
- wai-extra >=3.0 && <3.1 - directory >=1.1 && <1.4
- yaml >=0.8 && <0.9 - warp >=3.0 && <3.3
- http-conduit >=2.1 && <2.3 - data-default
- directory >=1.1 && <1.4 - aeson >=0.6 && <1.3
- warp >=3.0 && <3.3 - conduit >=1.0 && <2.0
- data-default - monad-logger >=0.3 && <0.4
- aeson >=0.6 && <1.3 - fast-logger >=2.2 && <2.5
- conduit >=1.0 && <2.0 - wai-logger >=2.2 && <2.4
- monad-logger >=0.3 && <0.4 - file-embed
- fast-logger >=2.2 && <2.5 - safe
- wai-logger >=2.2 && <2.4 - unordered-containers
- file-embed - containers
- safe - vector
- unordered-containers - time
- containers - case-insensitive
- vector - wai
- time - cryptonite
- case-insensitive - cryptonite-conduit
- wai - saltine
- cryptonite - base64-bytestring
- cryptonite-conduit - memory
- saltine - http-api-data
- base64-bytestring - profunctors
- memory - colonnade >=1.1.1
- http-api-data - yesod-colonnade >=1.1.0
- profunctors - blaze-markup
- colonnade >=1.1.1 - zip-stream
- yesod-colonnade >=1.1.0 - filepath
- blaze-markup - transformers
- zip-stream - wl-pprint-text
- filepath - uuid-types
- transformers - path-pieces
- wl-pprint-text - uuid-crypto
- uuid-types - filepath-crypto
- path-pieces - cryptoids-types
- uuid-crypto - cryptoids
- filepath-crypto - cryptoids-class
- cryptoids-types - binary
- cryptoids - cereal
- cryptoids-class - mtl
- binary - sandi
- cereal - esqueleto
- mtl - mime-types
- sandi - generic-deriving
- esqueleto - blaze-html
- mime-types - conduit-resumablesink >=0.2
- generic-deriving - parsec
- blaze-html - uuid
- conduit-resumablesink >=0.2 - exceptions
- parsec - stm
- uuid - stm-chans
- exceptions - stm-conduit
- stm - lens
- stm-chans - MonadRandom
- stm-conduit - email-validate
- lens - scientific
- MonadRandom - tz
- email-validate - system-locale
- scientific - th-lift-instances
- tz - gitrev
- system-locale - Glob
- th-lift-instances - ldap-client
- gitrev - connection
- Glob - universe
- ldap-client - universe-base
- connection - random
- universe - random-shuffle
- universe-base - th-abstraction
- random - HaskellNet
- random-shuffle - HaskellNet-SSL
- th-abstraction - network
- HaskellNet - resource-pool
- HaskellNet-SSL - mime-mail
- network - hashable
- resource-pool - aeson-pretty
- mime-mail - resourcet
- hashable - postgresql-simple
- aeson-pretty - word24
- resourcet - mmorph
- postgresql-simple - clientsession
- word24
- mmorph
- clientsession
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving
@ -159,24 +156,31 @@ default-extensions:
- BinaryLiterals - BinaryLiterals
- PolyKinds - PolyKinds
ghc-options:
- -Wall
- -fno-warn-type-defaults
- -fno-warn-partial-type-signatures
when:
- condition: flag(pedantic)
ghc-options:
- -Werror
- -fwarn-tabs
# 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.
library: library:
source-dirs: src source-dirs: src
when: when:
- condition: (flag(dev)) || (flag(library-only)) - condition: (flag(dev)) || (flag(library-only))
then: then:
ghc-options: ghc-options:
- -Wall - -O0
- -fwarn-tabs - -ddump-splices
- -O0 cpp-options: -DDEVELOPMENT
- -ddump-splices else:
cpp-options: -DDEVELOPMENT ghc-options:
else: - -O2
ghc-options:
- -Wall
- -fwarn-tabs
- -O2
# Runnable executable for our application # Runnable executable for our application
executables: executables:
@ -184,28 +188,36 @@ executables:
main: main.hs main: main.hs
source-dirs: app source-dirs: app
ghc-options: ghc-options:
- -threaded - -threaded
- -rtsopts - -rtsopts
- -with-rtsopts=-N - -with-rtsopts=-N
dependencies: dependencies:
- uniworx - uniworx
when: when:
- condition: flag(library-only) - condition: flag(library-only)
buildable: false buildable: false
# Test suite # Test suite
tests: tests:
test: yesod:
main: Spec.hs main: Spec.hs
source-dirs: test source-dirs: test
ghc-options: -Wall
dependencies: dependencies:
- uniworx - uniworx
- hspec >=2.0.0 - hspec >=2.0.0
- QuickCheck - QuickCheck
- yesod-test - yesod-test
- conduit-extra - conduit-extra
- quickcheck-instances - quickcheck-instances
hlint:
main: Hlint.hs
other-modules: []
source-dirs: hlint
dependencies:
- hlint-test
when:
- condition: "!flag(pedantic)"
buildable: false
# Define flags used by "yesod devel" to make compilation faster # Define flags used by "yesod devel" to make compilation faster
flags: flags:
@ -217,3 +229,7 @@ flags:
description: Turn on development settings, like auto-reload templates. description: Turn on development settings, like auto-reload templates.
manual: false manual: false
default: false default: false
pedantic:
description: Be very pedantic about warnings and errors
manual: true
default: true

8
routes
View File

@ -34,7 +34,7 @@
/ HomeR GET !free / HomeR GET !free
/users UsersR GET -- no tags, i.e. admins only /users UsersR GET -- no tags, i.e. admins only
/admin/test AdminTestR GET POST /admin/test AdminTestR GET POST
/admin/user/#CryptoUUIDUser AdminUserR GET /admin/user/#CryptoUUIDUser AdminUserR GET !development
/admin/user/#CryptoUUIDUser/hijack AdminHijackUserR POST /admin/user/#CryptoUUIDUser/hijack AdminHijackUserR POST
/admin/errMsg AdminErrMsgR GET POST /admin/errMsg AdminErrMsgR GET POST
/info VersionR GET !free /info VersionR GET !free
@ -50,8 +50,8 @@
!/term/#TermId TermCourseListR GET !free !/term/#TermId TermCourseListR GET !free
!/term/#TermId/#SchoolId TermSchoolCourseListR GET !free !/term/#TermId/#SchoolId TermSchoolCourseListR GET !free
/school SchoolListR GET /school SchoolListR GET !development
/school/#SchoolId SchoolShowR GET /school/#SchoolId SchoolShowR GET !development
-- For Pattern Synonyms see Foundation -- For Pattern Synonyms see Foundation
@ -64,7 +64,7 @@
/edit CEditR GET POST /edit CEditR GET POST
/delete CDeleteR GET POST !lecturerANDempty /delete CDeleteR GET POST !lecturerANDempty
/users CUsersR GET /users CUsersR GET
/user/#CryptoUUIDUser CUserR GET /user/#CryptoUUIDUser CUserR GET !development
/correctors CHiWisR GET /correctors CHiWisR GET
/subs CCorrectionsR GET POST /subs CCorrectionsR GET POST
/ex SheetListR GET !registered !materials /ex SheetListR GET !registered !materials

View File

@ -22,7 +22,7 @@ let
''; '';
override = oldAttrs: { override = oldAttrs: {
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ postgresql cabal-install ]) ++ (with haskellPackages; [ stack yesod-bin ]); nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ postgresql ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install ]);
shellHook = '' shellHook = ''
export PROMPT_INFO="${oldAttrs.name}" export PROMPT_INFO="${oldAttrs.name}"

View File

@ -30,9 +30,11 @@ import Network.Wai.Middleware.RequestLogger (Destination (Logger),
IPAddrSource (..), IPAddrSource (..),
OutputFormat (..), destination, OutputFormat (..), destination,
mkRequestLogger, outputFormat) mkRequestLogger, outputFormat)
import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet, import System.Log.FastLogger (defaultBufSize, newStderrLoggerSet,
toLogStr) toLogStr)
import qualified Data.Map.Strict as Map
import Foreign.Store import Foreign.Store
import qualified Data.UUID as UUID import qualified Data.UUID as UUID
@ -94,22 +96,20 @@ mkYesodDispatch "UniWorX" resourcesUniWorX
-- the place to put your migrate statements to have automatic database -- the place to put your migrate statements to have automatic database
-- migrations handled by Yesod. -- migrations handled by Yesod.
makeFoundation :: (MonadResource m, MonadBaseControl IO m) => AppSettings -> m UniWorX makeFoundation :: (MonadResource m, MonadBaseControl IO m) => AppSettings -> m UniWorX
makeFoundation appSettings@(AppSettings{..}) = do makeFoundation appSettings@AppSettings{..} = do
-- Some basic initializations: HTTP connection manager, logger, and static -- Some basic initializations: HTTP connection manager, logger, and static
-- subsite. -- subsite.
appHttpManager <- newManager appHttpManager <- newManager
appLogger <- liftIO $ do appLogger <- liftIO $ do
tgetter <- newTimeCache "%Y-%m-%d %T %z" tgetter <- newTimeCache "%Y-%m-%d %T %z"
loggerSet <- newStdoutLoggerSet defaultBufSize loggerSet <- newStderrLoggerSet defaultBufSize
return $ Yesod.Logger loggerSet tgetter return $ Yesod.Logger loggerSet tgetter
appStatic <- liftIO $ bool static staticDevel appMutableStatic appStaticDir appStatic <- liftIO $ bool static staticDevel appMutableStatic appStaticDir
appInstanceID <- liftIO $ maybe UUID.nextRandom (either readInstanceIDFile return) appInitialInstanceID appInstanceID <- liftIO $ maybe UUID.nextRandom (either readInstanceIDFile return) appInitialInstanceID
(appJobCtl, recvChans) <- fmap unzip . atomically . replicateM appJobWorkers $ do appJobCtl <- liftIO $ newTVarIO Map.empty
chan <- newBroadcastTMChan appCronThread <- liftIO newEmptyTMVarIO
recvChan <- dupTMChan chan
return (chan, recvChan)
appLogSettings <- liftIO $ newTVarIO appInitialLogSettings appLogSettings <- liftIO $ newTVarIO appInitialLogSettings
@ -149,7 +149,7 @@ makeFoundation appSettings@(AppSettings{..}) = do
let foundation = mkFoundation sqlPool smtpPool appCryptoIDKey appSessionKey appErrorMsgKey let foundation = mkFoundation sqlPool smtpPool appCryptoIDKey appSessionKey appErrorMsgKey
handleJobs recvChans foundation handleJobs foundation
-- Return the foundation -- Return the foundation
return foundation return foundation
@ -208,7 +208,7 @@ createSmtpPool SmtpConf{ smtpPool = ResourcePoolConf{..}, .. } = do
applyAuth SmtpAuthConf{..} conn = withLogging $ do applyAuth SmtpAuthConf{..} conn = withLogging $ do
$logDebugS "SMTP" "Doing authentication" $logDebugS "SMTP" "Doing authentication"
authSuccess <- liftIO $ SMTP.authenticate smtpAuthType smtpAuthUsername smtpAuthPassword conn authSuccess <- liftIO $ SMTP.authenticate smtpAuthType smtpAuthUsername smtpAuthPassword conn
when (not authSuccess) $ do unless authSuccess $
fail "SMTP authentication failed" fail "SMTP authentication failed"
return conn return conn
liftIO $ createPool (mkConnection >>= maybe return applyAuth smtpAuth) reapConnection poolStripes poolTimeout poolLimit liftIO $ createPool (mkConnection >>= maybe return applyAuth smtpAuth) reapConnection poolStripes poolTimeout poolLimit
@ -322,8 +322,7 @@ getApplicationRepl = do
return (getPort wsettings, foundation, app1) return (getPort wsettings, foundation, app1)
shutdownApp :: MonadIO m => UniWorX -> m () shutdownApp :: MonadIO m => UniWorX -> m ()
shutdownApp UniWorX{..} = do shutdownApp = stopJobCtl
liftIO . atomically $ mapM_ closeTMChan appJobCtl
--------------------------------------------- ---------------------------------------------

View File

@ -1,5 +1,6 @@
module Cron module Cron
( CronNextMatch(..) ( evalCronMatch
, CronNextMatch(..)
, nextCronMatch , nextCronMatch
, module Cron.Types , module Cron.Types
) where ) where
@ -18,11 +19,7 @@ import Data.Ratio ((%))
import qualified Data.Set as Set import qualified Data.Set as Set
import Data.List.NonEmpty (NonEmpty(..)) import Utils.Lens hiding (from, to)
import qualified Data.List.NonEmpty as NonEmpty
import Utils.Lens.TH
import Control.Lens
data CronDate = CronDate data CronDate = CronDate
@ -38,7 +35,7 @@ makeLenses_ ''CronDate
evalCronMatch :: CronMatch -> Natural -> Bool evalCronMatch :: CronMatch -> Natural -> Bool
evalCronMatch CronMatchAny _ = True evalCronMatch CronMatchAny _ = True
evalCronMatch CronMatchNone _ = False evalCronMatch CronMatchNone _ = False
evalCronMatch (CronMatchSome set) x = Set.member x $ toNullable set evalCronMatch (CronMatchSome xs) x = Set.member x $ toNullable xs
evalCronMatch (CronMatchStep step) x = (x `mod` step) == 0 evalCronMatch (CronMatchStep step) x = (x `mod` step) == 0
evalCronMatch (CronMatchContiguous from to) x = from <= x && x <= to evalCronMatch (CronMatchContiguous from to) x = from <= x && x <= to
evalCronMatch (CronMatchIntersect a b) x = evalCronMatch a x && evalCronMatch b x evalCronMatch (CronMatchIntersect a b) x = evalCronMatch a x && evalCronMatch b x
@ -115,7 +112,7 @@ genMatch :: Int -- ^ Period
-> [Natural] -> [Natural]
genMatch p m st CronMatchAny = take p $ map (bool id (succ . (`mod` fromIntegral p)) m) [st..] genMatch p m st CronMatchAny = take p $ map (bool id (succ . (`mod` fromIntegral p)) m) [st..]
genMatch _ _ _ CronMatchNone = [] genMatch _ _ _ CronMatchNone = []
genMatch p m _ (CronMatchSome set) = take p . map (bool id (succ . (`mod` fromIntegral p)) m) . Set.toAscList $ toNullable set genMatch p m _ (CronMatchSome xs) = take p . map (bool id (succ . (`mod` fromIntegral p)) m) . Set.toAscList $ toNullable xs
genMatch p m st (CronMatchStep step) = do genMatch p m st (CronMatchStep step) = do
start <- [st..st + step] start <- [st..st + step]
guard $ (start `mod` step) == 0 guard $ (start `mod` step) == 0
@ -135,9 +132,9 @@ genMatch p m st (CronMatchIntersect aGen bGen)
mergeAnd [] _ = [] mergeAnd [] _ = []
mergeAnd _ [] = [] mergeAnd _ [] = []
mergeAnd (a:as) (b:bs) mergeAnd (a:as) (b:bs)
| a < b = mergeAnd as (b:bs) | a < b = mergeAnd as (b:bs)
| a == b = a : mergeAnd as bs | a == b = a : mergeAnd as bs
| a > b = mergeAnd (a:as) bs | otherwise = mergeAnd (a:as) bs
genMatch p m st (CronMatchUnion CronMatchNone other) = genMatch p m st other genMatch p m st (CronMatchUnion CronMatchNone other) = genMatch p m st other
genMatch p m st (CronMatchUnion other CronMatchNone) = genMatch p m st other genMatch p m st (CronMatchUnion other CronMatchNone) = genMatch p m st other
genMatch p m st (CronMatchUnion CronMatchAny _) = genMatch p m st CronMatchAny genMatch p m st (CronMatchUnion CronMatchAny _) = genMatch p m st CronMatchAny
@ -147,9 +144,9 @@ genMatch p m st (CronMatchUnion aGen bGen) = merge (genMatch p m st aGen) (genMa
merge [] bs = bs merge [] bs = bs
merge as [] = as merge as [] = as
merge (a:as) (b:bs) merge (a:as) (b:bs)
| a < b = a : merge as (b:bs) | a < b = a : merge as (b:bs)
| a == b = a : merge as bs | a == b = a : merge as bs
| a > b = b : merge (a:as) bs | otherwise = b : merge (a:as) bs
nextCronMatch :: TZ -- ^ Timezone of the `Cron`-Entry nextCronMatch :: TZ -- ^ Timezone of the `Cron`-Entry
-> Maybe UTCTime -- ^ Time of last execution of the job -> Maybe UTCTime -- ^ Time of last execution of the job
@ -166,7 +163,6 @@ nextCronMatch tz mPrev now c@Cron{..} = case notAfter of
| otherwise -> MatchNone | otherwise -> MatchNone
MatchNone -> nextMatch MatchNone -> nextMatch
where where
nextMatch = nextCronMatch' tz mPrev now c
notAfter notAfter
| Right c' <- cronNotAfter | Right c' <- cronNotAfter
, Just ref <- notAfterRef , Just ref <- notAfterRef
@ -178,34 +174,34 @@ nextCronMatch tz mPrev now c@Cron{..} = case notAfter of
notAfterRef notAfterRef
| Just prevT <- mPrev = Just prevT | Just prevT <- mPrev = Just prevT
| otherwise = case execRef' now False cronInitial of | otherwise = case execRef' now False cronInitial of
MatchAsap -> error "execRef' should not return MatchAsap"
MatchAt t -> Just t MatchAt t -> Just t
MatchNone -> Nothing MatchNone -> Nothing
nextMatch = case mPrev of
nextCronMatch' tz mPrev now c@Cron{..} Nothing
| isNothing mPrev -> execRef now False cronInitial
= execRef now False cronInitial Just prevT
| Just prevT <- mPrev -> case cronRepeat of
= case cronRepeat of CronRepeatOnChange
CronRepeatOnChange | not $ matchesCron tz Nothing prevT c
| not $ matchesCron tz Nothing prevT c -> let
-> let cutoffTime = addUTCTime cronRateLimit prevT
cutoffTime = addUTCTime cronRateLimit prevT in case execRef now False cronInitial of
in case execRef now False cronInitial of MatchAsap
MatchAsap | now < cutoffTime -> MatchAt cutoffTime
| now < cutoffTime -> MatchAt cutoffTime MatchAt ts
MatchAt ts | ts < cutoffTime -> MatchAt cutoffTime
| ts < cutoffTime -> MatchAt cutoffTime other -> other
other -> other CronRepeatScheduled cronNext
CronRepeatScheduled cronNext -> case cronNext of
-> case cronNext of CronAsap
CronAsap | addUTCTime cronRateLimit prevT <= now
| addUTCTime cronRateLimit prevT <= now -> MatchAsap
-> MatchAsap | otherwise
| otherwise -> MatchAt $ addUTCTime cronRateLimit prevT
-> MatchAt $ addUTCTime cronRateLimit prevT _other
cronNext -> execRef (addUTCTime cronRateLimit prevT) True cronNext
-> execRef (addUTCTime cronRateLimit prevT) True cronNext _other -> MatchNone
_other -> MatchNone
execRef ref wasExecd cronAbsolute = case execRef' ref wasExecd cronAbsolute of execRef ref wasExecd cronAbsolute = case execRef' ref wasExecd cronAbsolute of
MatchAt t MatchAt t
@ -219,19 +215,26 @@ nextCronMatch tz mPrev now c@Cron{..} = case notAfter of
| otherwise -> MatchNone | otherwise -> MatchNone
CronCalendar{..} -> listToMatch $ do CronCalendar{..} -> listToMatch $ do
let CronDate{..} = toCronDate $ utcToLocalTimeTZ tz ref let CronDate{..} = toCronDate $ utcToLocalTimeTZ tz ref
cronYear <- genMatch 400 False cdYear cronYear
cronWeekOfYear <- genMatch 53 True cdWeekOfYear cronWeekOfYear mCronYear <- genMatch 400 False cdYear cronYear
cronDayOfYear <- genMatch 366 True cdDayOfYear cronDayOfYear mCronWeekOfYear <- genMatch 53 True cdWeekOfYear cronWeekOfYear
cronMonth <- genMatch 12 True cdMonth cronMonth mCronDayOfYear <- genMatch 366 True cdDayOfYear cronDayOfYear
cronWeekOfMonth <- genMatch 5 True cdWeekOfMonth cronWeekOfMonth mCronMonth <- genMatch 12 True cdMonth cronMonth
cronDayOfMonth <- genMatch 31 True cdDayOfMonth cronDayOfMonth mCronWeekOfMonth <- genMatch 5 True cdWeekOfMonth cronWeekOfMonth
cronDayOfWeek <- genMatch 7 True cdDayOfWeek cronDayOfWeek mCronDayOfMonth <- genMatch 31 True cdDayOfMonth cronDayOfMonth
cronHour <- genMatch 24 True cdHour cronHour mCronDayOfWeek <- genMatch 7 True cdDayOfWeek cronDayOfWeek
cronMinute <- genMatch 60 True cdMinute cronMinute mCronHour <- genMatch 24 True cdHour cronHour
cronSecond <- genMatch 60 True cdSecond cronSecond mCronMinute <- genMatch 60 True cdMinute cronMinute
guard $ consistentCronDate CronDate{..} mCronSecond <- genMatch 60 True cdSecond cronSecond
localDay <- maybeToList $ fromGregorianValid (fromIntegral cronYear) (fromIntegral cronMonth) (fromIntegral cronDayOfMonth) guard $ consistentCronDate CronDate
let localTimeOfDay = TimeOfDay (fromIntegral cronHour) (fromIntegral cronMinute) (fromIntegral cronSecond) { cdYear = mCronYear, cdMonth = mCronMonth, cdDayOfMonth = mCronDayOfMonth
, cdHour = mCronHour, cdMinute = mCronMinute, cdSecond = mCronSecond
, cdWeekOfYear = mCronWeekOfYear, cdWeekOfMonth = mCronWeekOfMonth
, cdDayOfYear = mCronDayOfYear, cdDayOfWeek = mCronDayOfWeek
}
localDay <- maybeToList $ fromGregorianValid (fromIntegral mCronYear) (fromIntegral mCronMonth) (fromIntegral mCronDayOfMonth)
let localTimeOfDay = TimeOfDay (fromIntegral mCronHour) (fromIntegral mCronMinute) (fromIntegral mCronSecond)
return $ localTimeToUTCTZ tz LocalTime{..} return $ localTimeToUTCTZ tz LocalTime{..}
CronNotScheduled -> MatchNone CronNotScheduled -> MatchNone

View File

@ -9,7 +9,7 @@ module CryptoID
import CryptoID.TH import CryptoID.TH
import ClassyPrelude hiding (fromString) import ClassyPrelude
import Model import Model
import qualified Data.CryptoID as E import qualified Data.CryptoID as E

View File

@ -24,4 +24,4 @@ instance (Eq a, Hashable a, Finite a, FromJSON b, FromJSONKey a) => FromJSON (a
vMap <- parseJSON val :: Parser (HashMap a b) vMap <- parseJSON val :: Parser (HashMap a b)
unless (HashSet.fromMap (HashMap.map (const ()) vMap) == HashSet.fromList universeF) $ unless (HashSet.fromMap (HashMap.map (const ()) vMap) == HashSet.fromList universeF) $
fail "Not all required keys found" fail "Not all required keys found"
return $ (vMap !) return (vMap !)

View File

@ -1,4 +1,5 @@
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} -- MonadCrypto
module Foundation where module Foundation where
@ -10,20 +11,18 @@ import Text.Jasmine (minifym)
import qualified Web.ClientSession as ClientSession import qualified Web.ClientSession as ClientSession
import Yesod.Auth.Message import Yesod.Auth.Message
import Yesod.Auth.Dummy
import Auth.LDAP import Auth.LDAP
import Auth.PWHash import Auth.PWHash
import Auth.Dummy import Auth.Dummy
import Jobs.Types import Jobs.Types
import qualified Network.Wai as W (requestMethod, pathInfo) import qualified Network.Wai as W (pathInfo)
import Yesod.Default.Util (addStaticContentExternal) import Yesod.Default.Util (addStaticContentExternal)
import Yesod.Core.Types (Logger) import Yesod.Core.Types (Logger)
import qualified Yesod.Core.Unsafe as Unsafe import qualified Yesod.Core.Unsafe as Unsafe
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Text.Encoding as TE
import qualified Data.CryptoID as E import qualified Data.CryptoID as E
@ -40,12 +39,10 @@ import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import Data.List (foldr1) import Data.List (foldr1)
import qualified Data.List as List
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
import Data.Map (Map, (!?)) import Data.Map (Map, (!?))
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.List (findIndex)
import Data.Monoid (Any(..)) import Data.Monoid (Any(..))
@ -61,22 +58,14 @@ import Control.Monad.Trans.Maybe (MaybeT(..))
import Control.Monad.Trans.Reader (runReader, mapReaderT) import Control.Monad.Trans.Reader (runReader, mapReaderT)
import Control.Monad.Trans.Writer (WriterT(..)) import Control.Monad.Trans.Writer (WriterT(..))
import Control.Monad.Writer.Class (MonadWriter(..)) import Control.Monad.Writer.Class (MonadWriter(..))
import Control.Monad.Catch (handleAll)
import qualified Control.Monad.Catch as C import qualified Control.Monad.Catch as C
import System.FilePath
import Handler.Utils.Templates
import Handler.Utils.StudyFeatures import Handler.Utils.StudyFeatures
import Control.Lens import Control.Lens
import Utils
import Utils.Form import Utils.Form
import Utils.Lens
import Utils.SystemMessage import Utils.SystemMessage
import Data.Aeson hiding (Error, Success) import Data.Aeson hiding (Error, Success)
import Data.Aeson.TH
import qualified Data.Yaml as Yaml
import Text.Shakespeare.Text (st) import Text.Shakespeare.Text (st)
@ -118,7 +107,8 @@ data UniWorX = UniWorX
, appLogSettings :: TVar LogSettings , appLogSettings :: TVar LogSettings
, appCryptoIDKey :: CryptoIDKey , appCryptoIDKey :: CryptoIDKey
, appInstanceID :: InstanceId , appInstanceID :: InstanceId
, appJobCtl :: [TMChan JobCtl] , appJobCtl :: TVar (Map ThreadId (TMChan JobCtl))
, appCronThread :: TMVar (ReleaseKey, ThreadId)
, appErrorMsgKey :: SecretBox.Key , appErrorMsgKey :: SecretBox.Key
, appSessionKey :: ClientSession.Key , appSessionKey :: ClientSession.Key
} }
@ -146,9 +136,11 @@ type MsgRenderer = MsgRendererS UniWorX -- see Utils
type MailM a = MailT (HandlerT UniWorX IO) a type MailM a = MailT (HandlerT UniWorX IO) a
-- Pattern Synonyms for convenience -- Pattern Synonyms for convenience
pattern CSheetR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetR -> Route UniWorX
pattern CSheetR tid ssh csh shn ptn pattern CSheetR tid ssh csh shn ptn
= CourseR tid ssh csh (SheetR shn ptn) = CourseR tid ssh csh (SheetR shn ptn)
pattern CSubmissionR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionR -> Route UniWorX
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)
@ -211,9 +203,10 @@ instance RenderMessage UniWorX Load where
newtype MsgLanguage = MsgLanguage Lang newtype MsgLanguage = MsgLanguage Lang
deriving (Eq, Ord, Show, Read) deriving (Eq, Ord, Show, Read)
instance RenderMessage UniWorX MsgLanguage where instance RenderMessage UniWorX MsgLanguage where
renderMessage foundation ls (MsgLanguage lang) renderMessage foundation ls (MsgLanguage lang@(Text.splitOn "-" -> lang'))
| lang == "de-DE" = mr MsgGermanGermany | ["de", "DE"] <- lang' = mr MsgGermanGermany
| "de" `isPrefixOf` lang = mr MsgGerman | ("de" : _) <- lang' = mr MsgGerman
| otherwise = lang
where where
mr = renderMessage foundation ls mr = renderMessage foundation ls
@ -230,7 +223,7 @@ embedRenderMessage ''UniWorX ''SheetType ("SheetType" <>)
newtype SheetTypeComplete = SheetTypeComplete SheetType newtype SheetTypeComplete = SheetTypeComplete SheetType
instance RenderMessage UniWorX (SheetTypeComplete) where instance RenderMessage UniWorX (SheetTypeComplete) where
renderMessage foundation ls (SheetTypeComplete st) = case st of renderMessage foundation ls (SheetTypeComplete sheetType) = case sheetType of
NotGraded -> mr NotGraded NotGraded -> mr NotGraded
other -> mr (grading other) <> ", " <> mr other other -> mr (grading other) <> ", " <> mr other
where where
@ -288,8 +281,8 @@ orAR _ _ AuthenticationRequired = AuthenticationRequired
orAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedOr x y orAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedOr x y
-- and -- and
andAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedAnd x y andAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedAnd x y
andAR _ reason@(Unauthorized x) _ = reason andAR _ reason@(Unauthorized _) _ = reason
andAR _ _ reason@(Unauthorized x) = reason andAR _ _ reason@(Unauthorized _) = reason
andAR _ Authorized other = other andAR _ Authorized other = other
andAR _ AuthenticationRequired _ = AuthenticationRequired andAR _ AuthenticationRequired _ = AuthenticationRequired
@ -346,6 +339,14 @@ knownTags = Map.fromList -- should not throw exceptions, i.e. no getBy404 or req
allow <- appAllowDeprecated . appSettings <$> getYesod allow <- appAllowDeprecated . appSettings <$> getYesod
return $ bool (Unauthorized "Deprecated Route") Authorized allow return $ bool (Unauthorized "Deprecated Route") Authorized allow
) )
,("development", APHandler $ \r _ -> do
$logWarnS "AccessControl" ("route in development: " <> tshow r)
#ifdef DEVELOPMENT
return Authorized
#else
return $ Unauthorized "Route under development"
#endif
)
,("lecturer", APDB $ \route _ -> case route of ,("lecturer", APDB $ \route _ -> case route of
CourseR tid ssh csh _ -> exceptT return return $ do CourseR tid ssh csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
@ -414,7 +415,7 @@ knownTags = Map.fromList -- should not throw exceptions, i.e. no getBy404 or req
return Authorized return Authorized
CourseR tid ssh csh CRegisterR -> maybeT (unauthorizedI MsgUnauthorizedCourseTime) $ do CourseR tid ssh csh CRegisterR -> maybeT (unauthorizedI MsgUnauthorizedCourseTime) $ do
Entity cid Course{..} <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh Entity _ Course{courseRegisterFrom, courseRegisterTo} <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
cTime <- (NTop . Just) <$> liftIO getCurrentTime cTime <- (NTop . Just) <$> liftIO getCurrentTime
guard $ NTop courseRegisterFrom <= cTime guard $ NTop courseRegisterFrom <= cTime
&& NTop courseRegisterTo >= cTime && NTop courseRegisterTo >= cTime
@ -422,7 +423,7 @@ knownTags = Map.fromList -- should not throw exceptions, i.e. no getBy404 or req
MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageTime) $ do MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageTime) $ do
smId <- decrypt cID smId <- decrypt cID
SystemMessage{..} <- MaybeT $ get smId SystemMessage{systemMessageFrom, systemMessageTo} <- MaybeT $ get smId
cTime <- (NTop . Just) <$> liftIO getCurrentTime cTime <- (NTop . Just) <$> liftIO getCurrentTime
guard $ NTop systemMessageFrom <= cTime guard $ NTop systemMessageFrom <= cTime
&& NTop systemMessageTo >= cTime && NTop systemMessageTo >= cTime
@ -625,14 +626,14 @@ instance Yesod UniWorX where
errPage = case err of errPage = case err of
NotFound -> [whamlet|<p>_{MsgErrorResponseNotFound}|] NotFound -> [whamlet|<p>_{MsgErrorResponseNotFound}|]
InternalError err -> encrypted err [whamlet|<p .errMsg>#{err}|] InternalError err' -> encrypted err' [whamlet|<p .errMsg>#{err'}|]
InvalidArgs errs -> [whamlet| InvalidArgs errs -> [whamlet|
<ul> <ul>
$forall err <- errs $forall err' <- errs
<li .errMsg>#{err} <li .errMsg>#{err'}
|] |]
NotAuthenticated -> [whamlet|<p>_{MsgErrorResponseNotAuthenticated}|] NotAuthenticated -> [whamlet|<p>_{MsgErrorResponseNotAuthenticated}|]
PermissionDenied err -> [whamlet|<p .errMsg>#{err}|] PermissionDenied err' -> [whamlet|<p .errMsg>#{err'}|]
BadMethod method -> [whamlet|<p>_{MsgErrorResponseBadMethod (decodeUtf8 method)}|] BadMethod method -> [whamlet|<p>_{MsgErrorResponseBadMethod (decodeUtf8 method)}|]
fmap toTypedContent . siteLayout (Just . toHtml . mr $ ErrorResponseTitle err) $ do fmap toTypedContent . siteLayout (Just . toHtml . mr $ ErrorResponseTitle err) $ do
toWidget toWidget
@ -754,8 +755,8 @@ siteLayout headingOverride widget = do
asidenav = $(widgetFile "widgets/asidenav") asidenav = $(widgetFile "widgets/asidenav")
contentHeadline :: Maybe Widget contentHeadline :: Maybe Widget
contentHeadline = (toWidget <$> headingOverride) <|> (pageHeading =<< mcurrentRoute) contentHeadline = (toWidget <$> headingOverride) <|> (pageHeading =<< mcurrentRoute)
breadcrumbs :: Widget breadcrumbsWgt :: Widget
breadcrumbs = $(widgetFile "widgets/breadcrumbs") breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs")
pageactionprime :: Widget pageactionprime :: Widget
pageactionprime = $(widgetFile "widgets/pageactionprime") -- TODO: rename, since it also shows secondary pageActions now pageactionprime = $(widgetFile "widgets/pageactionprime") -- TODO: rename, since it also shows secondary pageActions now
-- functions to determine if there are page-actions (primary or secondary) -- functions to determine if there are page-actions (primary or secondary)
@ -794,11 +795,13 @@ applySystemMessages = liftHandlerT . runDB . runConduit $ selectSource [] [] .|
where where
applyMessage (Entity smId SystemMessage{..}) = maybeT_ $ do applyMessage (Entity smId SystemMessage{..}) = maybeT_ $ do
cID <- encrypt smId cID <- encrypt smId
void . assertM (== Authorized) . lift $ evalAccessDB (MessageR cID) False
let sessionKey = "sm-" <> tshow (ciphertext cID) let sessionKey = "sm-" <> tshow (ciphertext cID)
assertM (== Authorized) . lift $ evalAccessDB (MessageR cID) False _ <- assertM isNothing $ lookupSessionJson sessionKey :: MaybeT (YesodDB UniWorX) (Maybe ())
assertM isNothing (lookupSessionJson sessionKey :: MaybeT (YesodDB UniWorX) (Maybe ()))
setSessionJson sessionKey () setSessionJson sessionKey ()
(SystemMessage{..}, smTrans) <- MaybeT $ getSystemMessage appLanguages smId
(_, smTrans) <- MaybeT $ getSystemMessage appLanguages smId
let let
(summary, content) = case smTrans of (summary, content) = case smTrans of
Nothing -> (systemMessageSummary, systemMessageContent) Nothing -> (systemMessageSummary, systemMessageContent)
@ -1185,11 +1188,12 @@ pageActions (CorrectionsR) =
, menuItemModal = True , menuItemModal = True
, menuItemAccessCallback' = runDB $ do , menuItemAccessCallback' = runDB $ do
uid <- liftHandlerT requireAuthId uid <- liftHandlerT requireAuthId
[E.Value count] <- E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> do [E.Value corrCount] <- E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> do
E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId
E.where_ $ sheet E.^. SheetSubmissionMode E.==. E.val CorrectorSubmissions E.where_ $ sheet E.^. SheetSubmissionMode E.==. E.val CorrectorSubmissions
E.&&. sheetCorrector E.^. SheetCorrectorUser E.==. E.val uid
return E.countRows return E.countRows
return $ (count :: Int) /= 0 return $ (corrCount :: Int) /= 0
} }
, PageActionPrime $ MenuItem , PageActionPrime $ MenuItem
{ menuItemLabel = "Korrekturen eintragen" { menuItemLabel = "Korrekturen eintragen"
@ -1214,11 +1218,12 @@ pageActions (CorrectionsGradeR) =
, menuItemModal = True , menuItemModal = True
, menuItemAccessCallback' = runDB $ do , menuItemAccessCallback' = runDB $ do
uid <- liftHandlerT requireAuthId uid <- liftHandlerT requireAuthId
[E.Value count] <- E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> do [E.Value corrCount] <- E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> do
E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId
E.where_ $ sheet E.^. SheetSubmissionMode E.==. E.val CorrectorSubmissions E.where_ $ sheet E.^. SheetSubmissionMode E.==. E.val CorrectorSubmissions
E.&&. sheetCorrector E.^. SheetCorrectorUser E.==. E.val uid
return E.countRows return E.countRows
return $ (count :: Int) /= 0 return $ (corrCount :: Int) /= 0
} }
] ]
pageActions _ = [] pageActions _ = []
@ -1295,7 +1300,7 @@ pageHeading (CSheetR tid ssh csh shn SEditR)
= Just $ i18nHeading $ MsgSheetEditHead tid ssh csh shn = Just $ i18nHeading $ MsgSheetEditHead tid ssh csh shn
pageHeading (CSheetR tid ssh csh shn SDelR) pageHeading (CSheetR tid ssh csh shn SDelR)
= Just $ i18nHeading $ MsgSheetDelHead tid ssh csh shn = Just $ i18nHeading $ MsgSheetDelHead tid ssh csh shn
pageHeading (CSheetR tid ssh csh shn SSubsR) pageHeading (CSheetR _tid _ssh _csh shn SSubsR)
= Just $ i18nHeading $ MsgSubmissionsSheet shn = Just $ i18nHeading $ MsgSubmissionsSheet shn
pageHeading (CSheetR tid ssh csh shn SubmissionNewR) pageHeading (CSheetR tid ssh csh shn SubmissionNewR)
= Just $ i18nHeading $ MsgSubmissionEditHead tid ssh csh shn = Just $ i18nHeading $ MsgSubmissionEditHead tid ssh csh shn
@ -1307,7 +1312,7 @@ pageHeading (CSubmissionR tid ssh csh shn _ SubShowR) -- TODO: Rethink this one!
pageHeading (CSubmissionR tid ssh csh shn cid CorrectionR) pageHeading (CSubmissionR tid ssh csh shn cid CorrectionR)
= Just $ i18nHeading $ MsgCorrectionHead tid ssh csh shn cid = Just $ i18nHeading $ MsgCorrectionHead tid ssh csh shn cid
-- (CSubmissionR tid csh shn cid SubDownloadR) -- just a download -- (CSubmissionR tid csh shn cid SubDownloadR) -- just a download
pageHeading (CSheetR tid ssh csh shn SCorrR) pageHeading (CSheetR _tid _ssh _csh shn SCorrR)
= Just $ i18nHeading $ MsgCorrectorsHead shn = Just $ i18nHeading $ MsgCorrectorsHead shn
-- (CSheetR tid ssh csh shn SFileR) -- just for Downloads -- (CSheetR tid ssh csh shn SFileR) -- just for Downloads
@ -1550,7 +1555,7 @@ instance YesodMail UniWorX where
pool <- maybe (throwM MailNotAvailable) return =<< getsYesod appSmtpPool pool <- maybe (throwM MailNotAvailable) return =<< getsYesod appSmtpPool
withResource pool act withResource pool act
mailT ctx mail = defMailT ctx $ do mailT ctx mail = defMailT ctx $ do
setMailObjectId void setMailObjectId
setDateCurrent setDateCurrent
replaceMailHeader "Auto-Submitted" $ Just "auto-generated" replaceMailHeader "Auto-Submitted" $ Just "auto-generated"

View File

@ -92,7 +92,7 @@ postAdminTestR = do
^{emailWidget} ^{emailWidget}
|] |]
defaultLayout $ do defaultLayout $
-- setTitle "Uni2work Admin Testpage" -- setTitle "Uni2work Admin Testpage"
$(widgetFile "adminTest") $(widgetFile "adminTest")
@ -101,7 +101,7 @@ getAdminUserR :: CryptoUUIDUser -> Handler Html
getAdminUserR uuid = do getAdminUserR uuid = do
uid <- decrypt uuid uid <- decrypt uuid
User{..} <- runDB $ get404 uid User{..} <- runDB $ get404 uid
defaultLayout $ defaultLayout
[whamlet| [whamlet|
<h1>TODO <h1>TODO
<h2>Admin Page for User ^{nameWidget userDisplayName userSurname} <h2>Admin Page for User ^{nameWidget userDisplayName userSurname}
@ -130,7 +130,7 @@ postAdminErrMsgR = do
either (throwE . MsgErrMsgCouldNotDecodePlaintext . tshow) return $ Text.decodeUtf8' plainBS either (throwE . MsgErrMsgCouldNotDecodePlaintext . tshow) return $ Text.decodeUtf8' plainBS
defaultLayout $ defaultLayout
[whamlet| [whamlet|
$maybe t <- plaintext $maybe t <- plaintext
<pre style="white-space:pre-wrap; font-family:monospace"> <pre style="white-space:pre-wrap; font-family:monospace">

View File

@ -24,7 +24,7 @@ import Data.Semigroup (Sum(..))
-- import qualified Data.Text as T -- import qualified Data.Text as T
-- import Data.Function ((&)) -- import Data.Function ((&))
-- --
import Colonnade hiding (fromMaybe, singleton, bool) -- import Colonnade hiding (fromMaybe, singleton, bool)
-- import Yesod.Colonnade -- import Yesod.Colonnade
-- --
-- import qualified Data.UUID.Cryptographic as UUID -- import qualified Data.UUID.Cryptographic as UUID
@ -40,25 +40,19 @@ import qualified Database.Esqueleto as E
import Web.PathPieces import Web.PathPieces
import Text.Hamlet (ihamletFile) import Text.Hamlet (ihamletFile)
import Text.Blaze.Html (preEscapedToHtml)
import Database.Persist.Sql (updateWhereCount) import Database.Persist.Sql (updateWhereCount)
import Data.List (genericLength) import Data.List (genericLength)
import Data.CaseInsensitive (CI) import Control.Monad.Trans.Writer (WriterT(..), runWriter)
import qualified Data.CaseInsensitive as CI
import Control.Monad.Trans.Writer (Writer, WriterT(..), runWriter)
import Control.Monad.Writer.Class (MonadWriter(..))
import Control.Monad.Trans.RWS (RWST) import Control.Monad.Trans.RWS (RWST)
import Control.Monad.Trans.State (State, StateT(..), runState) import Control.Monad.Trans.State (State, runState)
import qualified Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import Data.Foldable (foldrM) import Data.Foldable (foldrM)
import Data.Traversable (for)
@ -131,16 +125,16 @@ colSubmittors = sortable Nothing (i18nCell MsgSubmissionUsers) $ \DBRow{ dbrOutp
tid = course ^. _3 tid = course ^. _3
ssh = course ^. _4 ssh = course ^. _4
link cid = CourseR tid ssh csh $ CUserR cid link cid = CourseR tid ssh csh $ CUserR cid
cell = listCell (Map.toList users) $ \(userId, (User{..}, mPseudo)) -> protoCell = listCell (Map.toList users) $ \(userId, (User{..}, mPseudo)) ->
anchorCellM (link <$> encrypt userId) $ case mPseudo of anchorCellM (link <$> encrypt userId) $ case mPseudo of
Nothing -> nameWidget userDisplayName userSurname Nothing -> nameWidget userDisplayName userSurname
Just p -> [whamlet|^{nameWidget userDisplayName userSurname} (#{review pseudonymText p})|] Just p -> [whamlet|^{nameWidget userDisplayName userSurname} (#{review pseudonymText p})|]
in cell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] in protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")]
colSMatrikel :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a) colSMatrikel :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colSMatrikel = sortable Nothing (i18nCell MsgMatrikelNr) $ \DBRow{ dbrOutput=(_, _, _, _, users) } -> let colSMatrikel = sortable Nothing (i18nCell MsgMatrikelNr) $ \DBRow{ dbrOutput=(_, _, _, _, users) } -> let
cell = listCell (Map.toList users) $ \(userId, (User{..}, _)) -> anchorCellM (AdminUserR <$> encrypt userId) (maybe mempty toWidget userMatrikelnummer) protoCell = listCell (Map.toList users) $ \(userId, (User{..}, _)) -> anchorCellM (AdminUserR <$> encrypt userId) (maybe mempty toWidget userMatrikelnummer)
in cell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] in protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")]
colRating :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a) colRating :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colRating = sortable (Just "rating") (i18nCell MsgRating) $ \DBRow{ dbrOutput=(Entity subId Submission{..}, Entity _ Sheet{..}, course, _, _) } -> colRating = sortable (Just "rating") (i18nCell MsgRating) $ \DBRow{ dbrOutput=(Entity subId Submission{..}, Entity _ Sheet{..}, course, _, _) } ->
@ -213,9 +207,9 @@ makeCorrectionsTable whereClause dbtColonnade psValidator dbtProj' = do
E.orderBy [E.asc $ user E.^. UserId] E.orderBy [E.asc $ user E.^. UserId]
return (user, pseudonym E.?. SheetPseudonymPseudonym) return (user, pseudonym E.?. SheetPseudonymPseudonym)
let let
submittorMap = foldr (\((Entity userId user, E.Value pseudo)) -> Map.insert userId (user, pseudo)) Map.empty submittors submittorMap = foldr (\(Entity userId user, E.Value pseudo) -> Map.insert userId (user, pseudo)) Map.empty submittors
dbtProj' (submission, sheet, (courseName, courseShorthand, courseTerm, courseSchool), mCorrector, submittorMap) dbtProj' (submission, sheet, (courseName, courseShorthand, courseTerm, courseSchool), mCorrector, submittorMap)
dbTable psValidator $ DBTable dbTable psValidator DBTable
{ dbtSQLQuery { dbtSQLQuery
, dbtColonnade , dbtColonnade
, dbtProj , dbtProj
@ -240,7 +234,7 @@ makeCorrectionsTable whereClause dbtColonnade psValidator dbtProj' = do
) )
, ( "assignedtime" , ( "assignedtime"
, SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> submission E.^. SubmissionRatingAssigned , SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> submission E.^. SubmissionRatingAssigned
) )
] ]
, dbtFilter = Map.fromList , dbtFilter = Map.fromList
[ ( "term" [ ( "term"
@ -290,7 +284,7 @@ correctionsR :: _ -> _ -> _ -> Map ActionCorrections (AForm (HandlerT UniWorX IO
correctionsR whereClause (formColonnade -> displayColumns) psValidator actions = do correctionsR whereClause (formColonnade -> displayColumns) psValidator actions = do
tableForm <- makeCorrectionsTable whereClause displayColumns psValidator return tableForm <- makeCorrectionsTable whereClause displayColumns psValidator return
((actionRes, table), tableEncoding) <- runFormPost $ \csrf -> do ((actionRes, table), tableEncoding) <- runFormPost $ \csrf -> do
((fmap (Map.keysSet . Map.filter id . getDBFormResult (const False)) -> selectionRes), table) <- tableForm csrf (fmap $ Map.keysSet . Map.filter id . getDBFormResult (const False) -> selectionRes, table) <- tableForm csrf
(actionRes, action) <- multiAction actions Nothing (actionRes, action) <- multiAction actions Nothing
return ((,) <$> actionRes <*> selectionRes, table <> action) return ((,) <$> actionRes <*> selectionRes, table <> action)
@ -307,12 +301,12 @@ correctionsR whereClause (formColonnade -> displayColumns) psValidator actions =
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
runDB $ do runDB $ do
alreadyAssigned <- selectList [SubmissionId <-. subs, SubmissionRatingBy !=. Nothing] [] alreadyAssigned <- selectList [SubmissionId <-. subs, SubmissionRatingBy !=. Nothing] []
when (not $ null alreadyAssigned) $ do unless (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 Warning =<< 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 unless (null unassigned) $ do
num <- updateWhereCount [SubmissionId <-. Set.toList unassigned] num <- updateWhereCount [SubmissionId <-. Set.toList unassigned]
[ SubmissionRatingBy =. Just uid [ SubmissionRatingBy =. Just uid
, SubmissionRatingAssigned =. Just now -- save, since only applies to unassigned , SubmissionRatingAssigned =. Just now -- save, since only applies to unassigned
@ -341,18 +335,18 @@ correctionsR whereClause (formColonnade -> displayColumns) psValidator actions =
subs <- mapM decrypt $ Set.toList subs' subs <- mapM decrypt $ Set.toList subs'
runDB $ do runDB $ do
alreadyAssigned <- selectList [SubmissionId <-. subs, SubmissionRatingBy !=. Nothing] [] alreadyAssigned <- selectList [SubmissionId <-. subs, SubmissionRatingBy !=. Nothing] []
when (not $ null alreadyAssigned) $ do unless (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 Warning =<< 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 unless (null unassigned) $ do
(assigned, unassigned) <- assignSubmissions shid (Just unassigned) (assigned, stillUnassigned) <- assignSubmissions shid (Just unassigned)
when (not $ null assigned) $ unless (null assigned) $
addMessageI Success $ MsgUpdatedAssignedCorrectorsAuto (fromIntegral $ Set.size assigned) addMessageI Success $ MsgUpdatedAssignedCorrectorsAuto (fromIntegral $ Set.size assigned)
when (not $ null unassigned) $ do unless (null stillUnassigned) $ do
mr <- (toHtml . ) <$> getMessageRender mr <- (toHtml . ) <$> getMessageRender
unassigned' <- forM (Set.toList unassigned) $ \sid -> (encrypt sid :: DB CryptoFileNameSubmission) unassigned' <- forM (Set.toList stillUnassigned) $ \sid -> encrypt sid :: DB CryptoFileNameSubmission
addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsNotAssignedAuto.hamlet") mr) addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsNotAssignedAuto.hamlet") mr)
redirect currentRoute redirect currentRoute
@ -491,7 +485,7 @@ postCorrectionR tid ssh csh shn cid = do
NotGraded -> pure Nothing NotGraded -> pure Nothing
_otherwise -> aopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType) _otherwise -> aopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType)
(fslpI MsgRatingPoints "Punktezahl") (fslpI MsgRatingPoints "Punktezahl")
(Just $ submissionRatingPoints) (Just submissionRatingPoints)
((corrResult, corrForm), corrEncoding) <- runFormPost . identForm FIDcorrection . renderAForm FormStandard $ (,,) ((corrResult, corrForm), corrEncoding) <- runFormPost . identForm FIDcorrection . renderAForm FormStandard $ (,,)
<$> areq checkBoxField (fslI MsgRatingDone) (Just $ submissionRatingDone Submission{..}) <$> areq checkBoxField (fslI MsgRatingDone) (Just $ submissionRatingDone Submission{..})
@ -506,19 +500,17 @@ postCorrectionR tid ssh csh shn cid = do
case corrResult of case corrResult of
FormMissing -> return () FormMissing -> return ()
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
FormSuccess (rated, ratingPoints, ratingComment) -> do FormSuccess (rated, ratingPoints', ratingComment') -> do
runDBJobs $ do runDBJobs $ do
uid <- liftHandlerT requireAuthId uid <- liftHandlerT requireAuthId
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
Submission{submissionRatingTime} <- getJust sub
update sub [ SubmissionRatingBy =. (uid <$ guard rated) update sub [ SubmissionRatingBy =. (uid <$ guard rated)
-- SJ: I don't think we need to update AssignedTime here, since this is just for correction upload -- SJ: I don't think we need to update AssignedTime here, since this is just for correction upload
-- , SubmissionRatingAssigned +=. (Just now) -- TODO: Should submissionRatingAssigned change here if userId changes? -- , SubmissionRatingAssigned +=. (Just now) -- TODO: Should submissionRatingAssigned change here if userId changes?
, SubmissionRatingTime =. (now <$ guard rated) , SubmissionRatingTime =. (now <$ guard rated)
, SubmissionRatingPoints =. ratingPoints , SubmissionRatingPoints =. ratingPoints'
, SubmissionRatingComment =. ratingComment , SubmissionRatingComment =. ratingComment'
] ]
addMessageI Success $ bool MsgRatingDeleted MsgRatingUpdated rated addMessageI Success $ bool MsgRatingDeleted MsgRatingUpdated rated
@ -532,10 +524,10 @@ postCorrectionR tid ssh csh shn cid = do
case uploadResult of case uploadResult of
FormMissing -> return () FormMissing -> return ()
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
FormSuccess fileSource -> do FormSuccess fileUploads -> do
uid <- requireAuthId uid <- requireAuthId
runDBJobs . runConduit $ transPipe (lift . lift) fileSource .| extractRatingsMsg .| sinkSubmission uid (Right sub) True void . runDBJobs . runConduit $ transPipe (lift . lift) fileUploads .| extractRatingsMsg .| sinkSubmission uid (Right sub) True
addMessageI Success MsgRatingFilesUpdated addMessageI Success MsgRatingFilesUpdated
redirect $ CSubmissionR tid ssh csh shn cid CorrectionR redirect $ CSubmissionR tid ssh csh shn cid CorrectionR
@ -556,7 +548,7 @@ getCorrectionUserR tid ssh csh shn cid = do
mr <- getMessageRender mr <- getMessageRender
let ratingComment = fmap Text.strip submissionRatingComment >>= (\c -> c <$ guard (not $ null c)) let ratingComment = fmap Text.strip submissionRatingComment >>= (\c -> c <$ guard (not $ null c))
sheetTypeDesc = mr sheetType sheetTypeDesc = mr sheetType
defaultLayout $ do defaultLayout $
$(widgetFile "correction-user") $(widgetFile "correction-user")
_ -> notFound _ -> notFound
@ -582,7 +574,7 @@ postCorrectionsUploadR = do
addMessage Success =<< withUrlRenderer ($(ihamletFile "templates/messages/correctionsUploaded.hamlet") mr) addMessage Success =<< withUrlRenderer ($(ihamletFile "templates/messages/correctionsUploaded.hamlet") mr)
defaultLayout $ do defaultLayout $
$(widgetFile "corrections-upload") $(widgetFile "corrections-upload")
getCorrectionsCreateR, postCorrectionsCreateR :: Handler Html getCorrectionsCreateR, postCorrectionsCreateR :: Handler Html
@ -595,7 +587,7 @@ postCorrectionsCreateR = do
E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val uid E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val uid
E.&&. sheet E.^. SheetSubmissionMode E.==. E.val CorrectorSubmissions E.&&. sheet E.^. SheetSubmissionMode E.==. E.val CorrectorSubmissions
E.orderBy [E.desc $ course E.^. CourseTerm, E.asc $ course E.^. CourseShorthand, E.desc $ sheet E.^. SheetActiveFrom] E.orderBy [E.desc $ course E.^. CourseTerm, E.asc $ course E.^. CourseShorthand, E.desc $ sheet E.^. SheetActiveFrom]
return $ (sheet E.^. SheetId, course E.^. CourseTerm, course E.^. CourseShorthand, sheet E.^. SheetName) return (sheet E.^. SheetId, course E.^. CourseTerm, course E.^. CourseShorthand, sheet E.^. SheetName)
mkOptList :: [(E.Value SheetId, E.Value TermId, E.Value CourseShorthand, E.Value SheetName)] -> Handler (OptionList SheetId) mkOptList :: [(E.Value SheetId, E.Value TermId, E.Value CourseShorthand, E.Value SheetName)] -> Handler (OptionList SheetId)
mkOptList opts = do mkOptList opts = do
opts' <- mapM (\v@(E.Value sid, _, _, _) -> (, v) <$> encrypt sid) opts opts' <- mapM (\v@(E.Value sid, _, _, _) -> (, v) <$> encrypt sid) opts
@ -617,10 +609,9 @@ postCorrectionsCreateR = do
FormMissing -> return () FormMissing -> return ()
FormFailure errs -> forM_ errs $ addMessage Error . toHtml FormFailure errs -> forM_ errs $ addMessage Error . toHtml
FormSuccess (sid, pss) -> do FormSuccess (sid, pss) -> do
now <- liftIO getCurrentTime
runDB $ do runDB $ do
Sheet{..} <- get404 sid Sheet{..} <- get404 sid
(sps, unknown) <- fmap partition . forM pss . mapM $ \p -> maybe (Left p) Right <$> getBy (UniqueSheetPseudonym sid p) (sps, unknown) <- fmap partitionEithers' . forM pss . mapM $ \p -> maybe (Left p) Right <$> getBy (UniqueSheetPseudonym sid p)
forM_ unknown $ addMessageI Error . MsgUnknownPseudonym . review pseudonymText forM_ unknown $ addMessageI Error . MsgUnknownPseudonym . review pseudonymText
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let let
@ -640,12 +631,12 @@ postCorrectionsCreateR = do
, submissionRatingAssigned = Just now , submissionRatingAssigned = Just now
, submissionRatingTime = Nothing , submissionRatingTime = Nothing
} }
when (not $ null duplicate) unless (null duplicate)
$(addMessageFile Warning "templates/messages/submissionCreateDuplicates.hamlet") $(addMessageFile Warning "templates/messages/submissionCreateDuplicates.hamlet")
existingSubUsers <- E.select . E.from $ \submissionUser -> do existingSubUsers <- E.select . E.from $ \submissionUser -> do
E.where_ $ submissionUser E.^. SubmissionUserUser `E.in_` E.valList (sheetPseudonymUser <$> concat sps') E.where_ $ submissionUser E.^. SubmissionUserUser `E.in_` E.valList (sheetPseudonymUser <$> concat sps')
return submissionUser return submissionUser
when (not $ null existingSubUsers) $ do unless (null existingSubUsers) $ do
(Map.toList -> subs) <- foldrM (\(Entity _ SubmissionUser{..}) mp -> Map.insertWith (<>) <$> (encrypt submissionUserSubmission :: DB CryptoFileNameSubmission) <*> pure (Set.fromList . map sheetPseudonymPseudonym . filter (\SheetPseudonym{..} -> sheetPseudonymUser == submissionUserUser) $ concat sps') <*> pure mp) Map.empty existingSubUsers (Map.toList -> subs) <- foldrM (\(Entity _ SubmissionUser{..}) mp -> Map.insertWith (<>) <$> (encrypt submissionUserSubmission :: DB CryptoFileNameSubmission) <*> pure (Set.fromList . map sheetPseudonymPseudonym . filter (\SheetPseudonym{..} -> sheetPseudonymUser == submissionUserUser) $ concat sps') <*> pure mp) Map.empty existingSubUsers
$(addMessageFile Warning "templates/messages/submissionCreateExisting.hamlet") $(addMessageFile Warning "templates/messages/submissionCreateExisting.hamlet")
let sps'' = filter (not . null) $ filter (\spGroup -> not . flip any spGroup $ \SheetPseudonym{sheetPseudonymUser} -> sheetPseudonymUser `elem` map (submissionUserUser . entityVal) existingSubUsers) sps' let sps'' = filter (not . null) $ filter (\spGroup -> not . flip any spGroup $ \SheetPseudonym{sheetPseudonymUser} -> sheetPseudonymUser `elem` map (submissionUserUser . entityVal) existingSubUsers) sps'
@ -669,23 +660,18 @@ postCorrectionsCreateR = do
E.where_ . E.exists . E.from $ \submissionGroupUser -> E.where_ . E.exists . E.from $ \submissionGroupUser ->
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser `E.in_` E.valList (map sheetPseudonymUser spGroup) E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser `E.in_` E.valList (map sheetPseudonymUser spGroup)
return $ submissionGroup E.^. SubmissionGroupId return $ submissionGroup E.^. SubmissionGroupId
case (groups :: [E.Value SubmissionGroupId]) of if
[x] -> do | length (groups :: [E.Value SubmissionGroupId]) < 2
subId <- insert submission -> do
void . insert $ SubmissionEdit uid now subId subId <- insert submission
insertMany_ . flip map spGroup $ \SheetPseudonym{sheetPseudonymUser} -> SubmissionUser void . insert $ SubmissionEdit uid now subId
insertMany_ . flip map spGroup $ \SheetPseudonym{sheetPseudonymUser} -> SubmissionUser
{ submissionUserUser = sheetPseudonymUser { submissionUserUser = sheetPseudonymUser
, submissionUserSubmission = subId , submissionUserSubmission = subId
} }
[] -> do when (null groups) $
subId <- insert submission addMessageI Warning $ MsgSheetNoRegisteredGroup sheetGroupDesc
void . insert $ SubmissionEdit uid now subId | otherwise -> addMessageI Error $ MsgSheetAmbiguousRegisteredGroup sheetGroupDesc
insertMany_ . flip map spGroup $ \SheetPseudonym{sheetPseudonymUser} -> SubmissionUser
{ submissionUserUser = sheetPseudonymUser
, submissionUserSubmission = subId
}
addMessageI Warning $ MsgSheetNoRegisteredGroup sheetGroupDesc
_ -> addMessageI Error $ MsgSheetAmbiguousRegisteredGroup sheetGroupDesc
NoGroups NoGroups
| [SheetPseudonym{sheetPseudonymUser}] <- spGroup | [SheetPseudonym{sheetPseudonymUser}] <- spGroup
-> do -> do
@ -706,18 +692,18 @@ postCorrectionsCreateR = do
redirect CorrectionsGradeR redirect CorrectionsGradeR
defaultLayout $ do defaultLayout $
$(widgetFile "corrections-create") $(widgetFile "corrections-create")
where where
partition :: [[Either a b]] -> ([[b]], [a]) partitionEithers' :: [[Either a b]] -> ([[b]], [a])
partition = runWriter . mapM (WriterT . Identity . swap . partitionEithers) partitionEithers' = runWriter . mapM (WriterT . Identity . swap . partitionEithers)
textToList :: Textarea -> Handler (Either UniWorXMessage [[Pseudonym]]) textToList :: Textarea -> Handler (Either UniWorXMessage [[Pseudonym]])
textToList (map (map Text.strip . Text.splitOn ",") . filter (not . Text.null) . Text.lines . unTextarea -> ws) textToList (map (map Text.strip . Text.splitOn ",") . filter (not . Text.null) . Text.lines . unTextarea -> ws)
= let = let
invalid :: [Text] invalid :: [Text]
valid :: [[Pseudonym]] valid :: [[Pseudonym]]
(valid, invalid) = partition $ map (map $ \w -> maybe (Left w) Right $ w ^? pseudonymText) ws (valid, invalid) = partitionEithers' $ map (map $ \w -> maybe (Left w) Right $ w ^? pseudonymText) ws
in case invalid of in case invalid of
(i:_) -> return . Left $ MsgInvalidPseudonym i (i:_) -> return . Left $ MsgInvalidPseudonym i
[] -> return $ Right valid [] -> return $ Right valid
@ -749,7 +735,7 @@ postCorrectionsGradeR = do
cID <- encrypt subId cID <- encrypt subId
void . assertM (== Authorized) . lift $ evalAccessDB (CSubmissionR tid ssh csh shn cID CorrectionR) True void . assertM (== Authorized) . lift $ evalAccessDB (CSubmissionR tid ssh csh shn cID CorrectionR) True
return i return i
(((fmap unFormResult -> tableRes), table), tableEncoding) <- runFormPost tableForm ((fmap unFormResult -> tableRes, table), tableEncoding) <- runFormPost tableForm
case tableRes of case tableRes of
FormMissing -> return () FormMissing -> return ()
@ -765,9 +751,9 @@ postCorrectionsGradeR = do
, SubmissionRatingBy =. Just uid , SubmissionRatingBy =. Just uid
, SubmissionRatingTime =. now <$ guard rated , SubmissionRatingTime =. now <$ guard rated
] ]
| otherwise -> return $ Nothing | otherwise -> return Nothing
subs' <- traverse encrypt subs :: Handler [CryptoFileNameSubmission] subs' <- traverse encrypt subs :: Handler [CryptoFileNameSubmission]
unless (null subs') $(addMessageFile Success "templates/messages/correctionsUploaded.hamlet") unless (null subs') $(addMessageFile Success "templates/messages/correctionsUploaded.hamlet")
defaultLayout $ do defaultLayout $
$(widgetFile "corrections-grade") $(widgetFile "corrections-grade")

View File

@ -2,9 +2,7 @@ module Handler.Course where
import Import hiding (catMaybes) import Import hiding (catMaybes)
import Control.Lens
import Utils.Lens import Utils.Lens
import Utils.TH
-- import Utils.DB -- import Utils.DB
import Handler.Utils import Handler.Utils
import Handler.Utils.Table.Cells import Handler.Utils.Table.Cells
@ -20,20 +18,15 @@ import qualified Data.Map as Map
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Colonnade hiding (fromMaybe,bool)
-- import Yesod.Colonnade
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Data.UUID.Cryptographic as UUID
-- NOTE: Outdated way to use dbTable; see ProfileDataR Handler for a more recent method. -- NOTE: Outdated way to use dbTable; see ProfileDataR Handler for a more recent method.
type CourseTableData = DBRow (Entity Course, Int64, Bool, Entity School) type CourseTableData = DBRow (Entity Course, Int64, Bool, Entity School)
colCourse :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colCourse :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colCourse = sortable (Just "course") (i18nCell MsgCourse) colCourse = sortable (Just "course") (i18nCell MsgCourse)
$ \DBRow{ dbrOutput=(Entity cid Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR)
[whamlet|#{display courseName}|] [whamlet|#{display courseName}|]
@ -44,19 +37,19 @@ colCourseDescr = sortable (Just "course") (i18nCell MsgCourse) $ do
colDescription :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) colDescription :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colDescription = sortable Nothing (i18nCell MsgCourseDescription) colDescription = sortable Nothing (i18nCell MsgCourseDescription)
$ \DBRow{ dbrOutput=(Entity cid Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
case courseDescription of case courseDescription of
Nothing -> mempty Nothing -> mempty
(Just descr) -> cell $ modal "Beschreibung" (Right $ toWidget descr) (Just descr) -> cell $ modal "Beschreibung" (Right $ toWidget descr)
colCShort :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colCShort :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort) colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort)
$ \DBRow{ dbrOutput=(Entity cid Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|#{display courseShorthand}|] anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|#{display courseShorthand}|]
colCShortDescr :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colCShortDescr :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colCShortDescr = sortable (Just "cshort") (i18nCell MsgCourseShort) colCShortDescr = sortable (Just "cshort") (i18nCell MsgCourseShort)
$ \DBRow{ dbrOutput=(Entity cid Course{..}, _, _, _) } -> mappend $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> mappend
( anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|#{display courseShorthand}|] ) ( anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|#{display courseShorthand}|] )
( case courseDescription of ( case courseDescription of
Nothing -> mempty Nothing -> mempty
@ -70,7 +63,7 @@ colCShortDescr = sortable (Just "cshort") (i18nCell MsgCourseShort)
colTerm :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colTerm :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colTerm = sortable (Just "term") (i18nCell MsgTerm) colTerm = sortable (Just "term") (i18nCell MsgTerm)
$ \DBRow{ dbrOutput=(Entity cid Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
anchorCell (TermCourseListR courseTerm) [whamlet|#{display courseTerm}|] anchorCell (TermCourseListR courseTerm) [whamlet|#{display courseTerm}|]
colSchool :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colSchool :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
@ -85,24 +78,24 @@ colSchoolShort = sortable (Just "schoolshort") (i18nCell MsgCourseSchoolShort)
colRegFrom :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colRegFrom :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colRegFrom = sortable (Just "register-from") (i18nCell MsgRegisterFrom) colRegFrom = sortable (Just "register-from") (i18nCell MsgRegisterFrom)
$ \DBRow{ dbrOutput=(Entity cid Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
maybe mempty timeCell courseRegisterFrom maybe mempty timeCell courseRegisterFrom
-- cell $ traverse (formatTime SelFormatDateTime) courseRegisterFrom >>= maybe mempty toWidget -- cell $ traverse (formatTime SelFormatDateTime) courseRegisterFrom >>= maybe mempty toWidget
colRegTo :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colRegTo :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colRegTo = sortable (Just "register-to") (i18nCell MsgRegisterTo) colRegTo = sortable (Just "register-to") (i18nCell MsgRegisterTo)
$ \DBRow{ dbrOutput=(Entity cid Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
cell $ traverse (formatTime SelFormatDateTime) courseRegisterTo >>= maybe mempty toWidget maybe mempty timeCell courseRegisterTo
colParticipants :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colParticipants :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colParticipants = sortable (Just "participants") (i18nCell MsgCourseMembers) colParticipants = sortable (Just "participants") (i18nCell MsgCourseMembers)
$ \DBRow{ dbrOutput=(Entity cid Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of $ \DBRow{ dbrOutput=(Entity _ Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of
Nothing -> MsgCourseMembersCount currentParticipants Nothing -> MsgCourseMembersCount currentParticipants
Just max -> MsgCourseMembersCountLimited currentParticipants max Just limit -> MsgCourseMembersCountLimited currentParticipants limit
colRegistered :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colRegistered :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colRegistered = sortable (Just "registered") (i18nCell MsgRegistered) colRegistered = sortable (Just "registered") (i18nCell MsgRegistered)
$ \DBRow{ dbrOutput=(Entity cid Course{..}, _, registered, _) } -> tickmarkCell registered $ \DBRow{ dbrOutput=(_, _, registered, _) } -> tickmarkCell registered
type CourseTableExpr = E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity School) type CourseTableExpr = E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity School)
@ -112,7 +105,7 @@ course2Participants (course `E.InnerJoin` _school) = E.sub_select . E.from $ \co
return (E.countRows :: E.SqlExpr (E.Value Int64)) return (E.countRows :: E.SqlExpr (E.Value Int64))
course2Registered :: Maybe UserId -> CourseTableExpr -> E.SqlExpr (E.Value Bool) course2Registered :: Maybe UserId -> CourseTableExpr -> E.SqlExpr (E.Value Bool)
course2Registered muid (course `E.InnerJoin` _school) = E.exists . E.from $ \courseParticipant -> do course2Registered muid (course `E.InnerJoin` _school) = E.exists . E.from $ \courseParticipant ->
E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. course E.^. CourseId E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. course E.^. CourseId
E.&&. E.just (courseParticipant E.^. CourseParticipantUser) E.==. E.val muid E.&&. E.just (courseParticipant E.^. CourseParticipantUser) E.==. E.val muid
@ -129,7 +122,7 @@ makeCourseTable whereClause colChoices psValidator = do
return (course, participants, registered, school) return (course, participants, registered, school)
dbtProj :: DBRow _ -> MaybeT (ReaderT SqlBackend (HandlerT UniWorX IO)) CourseTableData dbtProj :: DBRow _ -> MaybeT (ReaderT SqlBackend (HandlerT UniWorX IO)) CourseTableData
dbtProj = traverse $ \(course, E.Value participants, E.Value registered, school) -> return (course, participants, registered, school) dbtProj = traverse $ \(course, E.Value participants, E.Value registered, school) -> return (course, participants, registered, school)
dbTable psValidator $ DBTable dbTable psValidator DBTable
{ dbtSQLQuery { dbtSQLQuery
, dbtColonnade = colChoices , dbtColonnade = colChoices
, dbtProj , dbtProj
@ -141,7 +134,7 @@ makeCourseTable whereClause colChoices psValidator = do
, ( "schoolshort", SortColumn $ \(_course `E.InnerJoin` school) -> school E.^. SchoolShorthand) , ( "schoolshort", SortColumn $ \(_course `E.InnerJoin` school) -> school E.^. SchoolShorthand)
, ( "register-from", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterFrom) , ( "register-from", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterFrom)
, ( "register-to", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterTo) , ( "register-to", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterTo)
, ( "participants", SortColumn $ course2Participants ) , ( "participants", SortColumn course2Participants )
, ( "registered", SortColumn $ course2Registered muid) , ( "registered", SortColumn $ course2Registered muid)
] ]
, dbtFilter = Map.fromList -- OverloadedLists does not work with the templates here , dbtFilter = Map.fromList -- OverloadedLists does not work with the templates here
@ -213,9 +206,9 @@ getTermSchoolCourseListR tid ssh = do
, colParticipants , colParticipants
, maybe mempty (const colRegistered) muid , maybe mempty (const colRegistered) muid
] ]
whereClause = \(course, _, _) -> whereClause (course, _, _) =
course E.^. CourseTerm E.==. E.val tid course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
validator = def validator = def
& defaultSorting [("cshort", SortAsc)] & defaultSorting [("cshort", SortAsc)]
((), coursesTable) <- makeCourseTable whereClause colonnade validator ((), coursesTable) <- makeCourseTable whereClause colonnade validator
@ -237,7 +230,7 @@ getTermCourseListR tid = do
, colParticipants , colParticipants
, maybe mempty (const colRegistered) muid , maybe mempty (const colRegistered) muid
] ]
whereClause = \(course, _, _) -> course E.^. CourseTerm E.==. E.val tid whereClause (course, _, _) = course E.^. CourseTerm E.==. E.val tid
validator = def validator = def
& defaultSorting [("cshort", SortAsc)] & defaultSorting [("cshort", SortAsc)]
((), coursesTable) <- makeCourseTable whereClause colonnade validator ((), coursesTable) <- makeCourseTable whereClause colonnade validator
@ -261,21 +254,21 @@ getCShowR tid ssh csh = do
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
return $ user E.^. UserDisplayName return $ user E.^. UserDisplayName
return $ (courseEnt,dependent,E.unValue <$> lecturers) return (courseEnt,dependent,E.unValue <$> lecturers)
let course = entityVal courseEnt let course = entityVal courseEnt
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerForm registered $ courseRegisterSecret course (regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerForm registered $ courseRegisterSecret course
registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid ssh csh CRegisterR) True registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid ssh csh CRegisterR) True
mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom course mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom course
mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course
defaultLayout $ do defaultLayout $ do
setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|] setTitle [shamlet| #{toPathPiece tid} - #{csh}|]
$(widgetFile "course") $(widgetFile "course")
registerForm :: Bool -> Maybe Text -> Form Bool registerForm :: Bool -> Maybe Text -> Form Bool
registerForm registered msecret extra = do registerForm registered msecret extra = do
(msecretRes', msecretView) <- case msecret of (msecretRes', msecretView) <- case msecret of
(Just _) | not registered -> bimap Just Just <$> (mreq textField (fslpI MsgCourseSecret "Code") Nothing) (Just _) | not registered -> bimap Just Just <$> mreq textField (fslpI MsgCourseSecret "Code") Nothing
_ -> return (Nothing,Nothing) _ -> return (Nothing,Nothing)
(btnRes, btnView) <- mreq (buttonField $ bool BtnRegister BtnDeregister registered) "buttonField ignores settings anyway" Nothing (btnRes, btnView) <- mreq (buttonField $ bool BtnRegister BtnDeregister registered) "buttonField ignores settings anyway" Nothing
let widget = $(widgetFile "widgets/registerForm") let widget = $(widgetFile "widgets/registerForm")
@ -289,7 +282,7 @@ postCRegisterR tid ssh csh = do
aid <- requireAuthId aid <- requireAuthId
(cid, course, registered) <- runDB $ do (cid, course, registered) <- runDB $ do
(Entity cid course) <- getBy404 $ TermSchoolCourseShort tid ssh csh (Entity cid course) <- getBy404 $ TermSchoolCourseShort tid ssh csh
registered <- isJust <$> (getBy $ UniqueParticipant aid cid) registered <- isJust <$> getBy (UniqueParticipant aid cid)
return (cid, course, registered) return (cid, course, registered)
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerForm registered $ courseRegisterSecret course ((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerForm registered $ courseRegisterSecret course
case regResult of case regResult of
@ -298,11 +291,11 @@ postCRegisterR tid ssh csh = do
runDB $ deleteBy $ UniqueParticipant aid cid runDB $ deleteBy $ UniqueParticipant aid cid
addMessageI Info MsgCourseDeregisterOk addMessageI Info MsgCourseDeregisterOk
| codeOk -> do | codeOk -> do
actTime <- liftIO $ getCurrentTime actTime <- liftIO getCurrentTime
regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime
when (isJust regOk) $ addMessageI Success MsgCourseRegisterOk when (isJust regOk) $ addMessageI Success MsgCourseRegisterOk
| otherwise -> addMessageI Warning MsgCourseSecretWrong | otherwise -> addMessageI Warning MsgCourseSecretWrong
(_other) -> return () -- TODO check this! _other -> return () -- TODO check this!
redirect $ CourseR tid ssh csh CShowR redirect $ CourseR tid ssh csh CShowR
@ -323,21 +316,20 @@ getCourseNewR = do
let noTemplateAction = courseEditHandler True Nothing let noTemplateAction = courseEditHandler True Nothing
case params of -- DO NOT REMOVE: without this distinction, lecturers would never see an empty newCourseForm any more! case params of -- DO NOT REMOVE: without this distinction, lecturers would never see an empty newCourseForm any more!
FormMissing -> noTemplateAction FormMissing -> noTemplateAction
FormFailure msgs -> forM_ msgs ((addMessage Error) . toHtml) >> FormFailure msgs -> forM_ msgs (addMessage Error . toHtml) >>
noTemplateAction noTemplateAction
FormSuccess (fmap TermKey -> mbTid, fmap SchoolKey -> mbSsh, mbCsh) -> do FormSuccess (fmap TermKey -> mbTid, fmap SchoolKey -> mbSsh, mbCsh) -> do
uid <- requireAuthId oldCourses <- runDB $
oldCourses <- runDB $ do
E.select $ E.from $ \course -> do E.select $ E.from $ \course -> do
whenIsJust mbTid $ \tid -> E.where_ $ course E.^. CourseTerm E.==. E.val tid whenIsJust mbTid $ \tid -> E.where_ $ course E.^. CourseTerm E.==. E.val tid
whenIsJust mbSsh $ \ssh -> E.where_ $ course E.^. CourseSchool E.==. E.val ssh whenIsJust mbSsh $ \ssh -> E.where_ $ course E.^. CourseSchool E.==. E.val ssh
whenIsJust mbCsh $ \csh -> E.where_ $ course E.^. CourseShorthand E.==. E.val csh whenIsJust mbCsh $ \csh -> E.where_ $ course E.^. CourseShorthand E.==. E.val csh
let lecturersCourse = let lecturersCourse =
E.exists $ E.from $ \lecturer -> do E.exists $ E.from $ \lecturer ->
E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid
E.&&. lecturer E.^. LecturerCourse E.==. course E.^. CourseId E.&&. lecturer E.^. LecturerCourse E.==. course E.^. CourseId
let lecturersSchool = let lecturersSchool =
E.exists $ E.from $ \user -> do E.exists $ E.from $ \user ->
E.where_ $ user E.^. UserLecturerUser E.==. E.val uid E.where_ $ user E.^. UserLecturerUser E.==. E.val uid
E.&&. user E.^. UserLecturerSchool E.==. course E.^. CourseSchool E.&&. user E.^. UserLecturerSchool E.==. course E.^. CourseSchool
let courseCreated c = let courseCreated c =
@ -351,7 +343,7 @@ getCourseNewR = do
return course return course
template <- case listToMaybe oldCourses of template <- case listToMaybe oldCourses of
(Just oldTemplate) -> (Just oldTemplate) ->
let newTemplate = (courseToForm oldTemplate) in let newTemplate = courseToForm oldTemplate in
return $ Just $ newTemplate return $ Just $ newTemplate
{ cfCourseId = Nothing { cfCourseId = Nothing
, cfTerm = TermKey $ TermIdentifier 0 Winter -- invalid, will be ignored; undefined won't work due to strictness , cfTerm = TermKey $ TermIdentifier 0 Winter -- invalid, will be ignored; undefined won't work due to strictness
@ -363,7 +355,7 @@ getCourseNewR = do
(tidOk,sshOk,cshOk) <- runDB $ (,,) (tidOk,sshOk,cshOk) <- runDB $ (,,)
<$> ifMaybeM mbTid True existsKey <$> ifMaybeM mbTid True existsKey
<*> ifMaybeM mbSsh True existsKey <*> ifMaybeM mbSsh True existsKey
<*> ifMaybeM mbCsh True (\csh -> (not . null) <$> selectKeysList [CourseShorthand ==. csh] [LimitTo 1]) <*> ifMaybeM mbCsh True (\csh -> not . null <$> selectKeysList [CourseShorthand ==. csh] [LimitTo 1])
unless tidOk $ addMessageI Warning $ MsgNoSuchTerm $ fromJust mbTid -- safe, since tidOk==True otherwise unless tidOk $ addMessageI Warning $ MsgNoSuchTerm $ fromJust mbTid -- safe, since tidOk==True otherwise
unless sshOk $ addMessageI Warning $ MsgNoSuchSchool $ fromJust mbSsh -- safe, since sshOk==True otherwise unless sshOk $ addMessageI Warning $ MsgNoSuchSchool $ fromJust mbSsh -- safe, since sshOk==True otherwise
unless cshOk $ addMessageI Warning $ MsgNoSuchCourseShorthand $ fromJust mbCsh unless cshOk $ addMessageI Warning $ MsgNoSuchCourseShorthand $ fromJust mbCsh
@ -403,19 +395,19 @@ postCDeleteR = error "TODO: implement getCDeleteR"
-- | Course Creation and Editing -- | Course Creation and Editing
-- | IMPORTANT: in case of Edit, Post/Get Request is provided with the same CourseForm template (cannot be Nothing), -- | IMPORTANT: in case of Edit, Post/Get Request is provided with the same CourseForm template (cannot be Nothing),
-- | since an edit is identified via cfCourseId which is not contained in the received form data for security reasons! -- | since an edit is identified via cfCourseId which is not contained in the received form data for security reasons!
courseEditHandler :: Bool -> Maybe CourseForm -> Handler Html courseEditHandler :: Bool -> Maybe CourseForm -> Handler Html -- FIXME: _isGet is not used
courseEditHandler isGet mbCourseForm = do courseEditHandler _isGet mbCourseForm = do
aid <- requireAuthId -- TODO: Verify that Editor is owner of the Course to be Edited!!! aid <- requireAuthId -- TODO: Verify that Editor is owner of the Course to be Edited!!!
((result, formWidget), formEnctype) <- runFormPost $ newCourseForm mbCourseForm ((result, formWidget), formEnctype) <- runFormPost $ newCourseForm mbCourseForm
case result of case result of
(FormSuccess res@( (FormSuccess res@CourseForm
CourseForm { cfCourseId = Nothing { cfCourseId = Nothing
, cfShort = csh , cfShort = csh
, cfSchool = ssh , cfSchool = ssh
, cfTerm = tid , cfTerm = tid
})) -> do -- create new course }) -> do -- create new course
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
insertOkay <- runDB $ insertUnique $ Course insertOkay <- runDB $ insertUnique Course
{ courseName = cfName res { courseName = cfName res
, courseDescription = cfDesc res , courseDescription = cfDesc res
, courseLinkExternal = cfLink res , courseLinkExternal = cfLink res
@ -439,34 +431,33 @@ courseEditHandler isGet mbCourseForm = do
Nothing -> Nothing ->
addMessageI Warning $ MsgCourseNewDupShort tid ssh csh addMessageI Warning $ MsgCourseNewDupShort tid ssh csh
(FormSuccess res@( (FormSuccess res@CourseForm
CourseForm { cfCourseId = Just cid { cfCourseId = Just cid
, cfShort = csh , cfShort = csh
, cfSchool = ssh , cfSchool = ssh
, cfTerm = tid , cfTerm = tid
})) -> do -- edit existing course }) -> do -- edit existing course
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
-- addMessage "debug" [shamlet| #{show res}|] -- addMessage "debug" [shamlet| #{show res}|]
success <- runDB $ do success <- runDB $ do
old <- get cid old <- get cid
case old of case old of
Nothing -> addMessageI Error MsgInvalidInput $> False Nothing -> addMessageI Error MsgInvalidInput $> False
(Just oldCourse) -> do (Just _) -> do
updOkay <- myReplaceUnique cid ( -- replaceUnique requires Eq Course, which we cannot have updOkay <- myReplaceUnique cid Course
Course { courseName = cfName res { courseName = cfName res
, courseDescription = cfDesc res , courseDescription = cfDesc res
, courseLinkExternal = cfLink res , courseLinkExternal = cfLink res
, courseShorthand = cfShort res , courseShorthand = cfShort res
, courseTerm = cfTerm res -- dangerous , courseTerm = cfTerm res -- dangerous
, courseSchool = cfSchool res , courseSchool = cfSchool res
, courseCapacity = cfCapacity res , courseCapacity = cfCapacity res
, courseRegisterSecret = cfSecret res , courseRegisterSecret = cfSecret res
, courseMaterialFree = cfMatFree res , courseMaterialFree = cfMatFree res
, courseRegisterFrom = cfRegFrom res , courseRegisterFrom = cfRegFrom res
, courseRegisterTo = cfRegTo res , courseRegisterTo = cfRegTo res
, courseDeregisterUntil = cfDeRegUntil res , courseDeregisterUntil = cfDeRegUntil res
} }
)
case updOkay of case updOkay of
(Just _) -> addMessageI Warning (MsgCourseEditDupShort tid ssh csh) $> False (Just _) -> addMessageI Warning (MsgCourseEditDupShort tid ssh csh) $> False
Nothing -> do Nothing -> do
@ -476,7 +467,7 @@ courseEditHandler isGet mbCourseForm = do
when success $ redirect $ CourseR tid ssh csh CShowR when success $ redirect $ CourseR tid ssh csh CShowR
(FormFailure _) -> addMessageI Warning MsgInvalidInput (FormFailure _) -> addMessageI Warning MsgInvalidInput
(FormMissing) -> return () FormMissing -> return ()
actionUrl <- fromMaybe CourseNewR <$> getCurrentRoute actionUrl <- fromMaybe CourseNewR <$> getCurrentRoute
defaultLayout $ do defaultLayout $ do
setTitleI MsgCourseEditTitle setTitleI MsgCourseEditTitle
@ -578,7 +569,7 @@ newCourseForm template = identForm FIDcourse $ \html -> do
validateCourse :: CourseForm -> [Text] validateCourse :: CourseForm -> [Text]
validateCourse (CourseForm{..}) = validateCourse CourseForm{..} =
[ msg | (False, msg) <- [ msg | (False, msg) <-
[ [
( NTop cfRegFrom <= NTop cfRegTo ( NTop cfRegFrom <= NTop cfRegTo
@ -598,18 +589,24 @@ validateCourse (CourseForm{..}) =
getCUsersR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCUsersR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCUsersR tid ssh csh = undefined -- TODO getCUsersR = error "CUsersR: Not implemented"
getCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html getCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html
getCUserR tid ssh csh uuid = do getCUserR _tid _ssh _csh uCId = do
uid <- decrypt uuid -- Needs authorization check:
--
-- - User is current member of course
-- - User has submitted in course
-- - User is member of registered group for course
-- - User is corrector for course (?)
-- - User is lecturer for course (?)
uid <- decrypt uCId
User{..} <- runDB $ get404 uid User{..} <- runDB $ get404 uid
defaultLayout $ defaultLayout -- TODO
[whamlet| [whamlet|
<h1>TODO <p>^{nameWidget userDisplayName userSurname}
<h2>Lecturer's Page for User ^{nameWidget userDisplayName userSurname}
|] |]
getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCHiWisR tid ssh csh = undefined -- TODO getCHiWisR = error "CHiWisR: Not implemented"

View File

@ -8,7 +8,6 @@ import qualified Data.Map as Map
import qualified Data.Text as Text import qualified Data.Text as Text
import Data.Text.Encoding (decodeUtf8') import Data.Text.Encoding (decodeUtf8')
import Data.Time hiding (formatTime) import Data.Time hiding (formatTime)
import Data.Universe
import Data.Universe.Helpers import Data.Universe.Helpers
import Network.Wai (requestHeaderReferer) import Network.Wai (requestHeaderReferer)
@ -56,43 +55,44 @@ homeAnonymous = do
let tableData :: E.SqlExpr (Entity Course) let tableData :: E.SqlExpr (Entity Course)
-> E.SqlQuery (E.SqlExpr (Entity Course)) -> E.SqlQuery (E.SqlExpr (Entity Course))
tableData course = do tableData course = do
E.where_ $ (E.not_ $ E.isNothing $ course E.^. CourseRegisterFrom) -- DO: do this with isAuthorized in dbtProj E.where_ $ E.not_ (E.isNothing $ course E.^. CourseRegisterFrom) -- DO: do this with isAuthorized in dbtProj
E.&&. (course E.^. CourseRegisterFrom E.<=. E.val (Just cTime)) E.&&. (course E.^. CourseRegisterFrom E.<=. E.val (Just cTime))
E.&&. ((E.isNothing $ course E.^. CourseRegisterTo) E.&&. ( E.isNothing (course E.^. CourseRegisterTo)
E.||. (course E.^. CourseRegisterTo E.>=. E.val (Just cTime))) E.||. course E.^. CourseRegisterTo E.>=. E.val (Just cTime)
)
return course return course
colonnade :: Colonnade Sortable (DBRow (Entity Course)) (DBCell (HandlerT UniWorX IO) ()) colonnade :: Colonnade Sortable (DBRow (Entity Course)) (DBCell (HandlerT UniWorX IO) ())
colonnade = mconcat colonnade = mconcat
[ -- dbRow [ -- dbRow
sortable (Just "term") (i18nCell MsgTerm) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> sortable (Just "term") (i18nCell MsgTerm) $ \DBRow{ dbrOutput=Entity{entityVal = course} } ->
textCell $ display $ courseTerm course textCell $ display $ courseTerm course
, sortable (Just "school") (i18nCell MsgCourseSchool) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> do , sortable (Just "school") (i18nCell MsgCourseSchool) $ \DBRow{ dbrOutput=Entity{entityVal = course} } ->
textCell $ display $ courseSchool course textCell $ display $ courseSchool course
, sortable (Just "course") (i18nCell MsgCourse) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> do , sortable (Just "course") (i18nCell MsgCourse) $ \DBRow{ dbrOutput=Entity{entityVal = course} } -> do
let tid = courseTerm course let tid = courseTerm course
ssh = courseSchool course ssh = courseSchool course
csh = courseShorthand course csh = courseShorthand course
anchorCell (CourseR tid ssh csh CShowR) (toWidget $ display csh) anchorCell (CourseR tid ssh csh CShowR) (toWidget $ display csh)
, sortable (Just "deadline") (i18nCell MsgRegisterTo) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> , sortable (Just "deadline") (i18nCell MsgRegisterTo) $ \DBRow{ dbrOutput=Entity{entityVal = course} } ->
cell $ traverse (formatTime SelFormatDateTime) (courseRegisterTo course) >>= maybe mempty toWidget cell $ traverse (formatTime SelFormatDateTime) (courseRegisterTo course) >>= maybe mempty toWidget
] ]
((), courseTable) <- dbTable def $ DBTable ((), courseTable) <- dbTable def DBTable
{ dbtSQLQuery = tableData { dbtSQLQuery = tableData
, dbtColonnade = colonnade , dbtColonnade = colonnade
, dbtProj = return , dbtProj = return
, dbtSorting = Map.fromList , dbtSorting = Map.fromList
[ ( "term" [ ( "term"
, SortColumn $ \(course) -> course E.^. CourseTerm , SortColumn $ \course -> course E.^. CourseTerm
) )
, ( "school" , ( "school"
, SortColumn $ \(course) -> course E.^. CourseSchool , SortColumn $ \course -> course E.^. CourseSchool
) )
, ( "course" , ( "course"
, SortColumn $ \(course) -> course E.^. CourseShorthand , SortColumn $ \course -> course E.^. CourseShorthand
) )
, ( "deadline" , ( "deadline"
, SortColumn $ \(course) -> course E.^. CourseRegisterTo , SortColumn $ \course -> course E.^. CourseRegisterTo
) )
] ]
, dbtFilter = mempty {- [ ( "term" , dbtFilter = mempty {- [ ( "term"
@ -106,7 +106,7 @@ homeAnonymous = do
} }
-- let features = $(widgetFile "featureList") -- let features = $(widgetFile "featureList")
-- addMessage Warning "Vorabversion! Die Implementierung von Uni2work ist noch nicht abgeschlossen!" -- addMessage Warning "Vorabversion! Die Implementierung von Uni2work ist noch nicht abgeschlossen!"
defaultLayout $ do defaultLayout
-- $(widgetFile "dsgvDisclaimer") -- $(widgetFile "dsgvDisclaimer")
$(widgetFile "home") $(widgetFile "home")
@ -126,7 +126,7 @@ homeUser uid = do
, E.SqlExpr (E.Value (Maybe SubmissionId))) , E.SqlExpr (E.Value (Maybe SubmissionId)))
tableData ((participant `E.InnerJoin` course `E.InnerJoin` sheet) `E.LeftOuterJoin` (submission `E.InnerJoin` subuser)) = do tableData ((participant `E.InnerJoin` course `E.InnerJoin` sheet) `E.LeftOuterJoin` (submission `E.InnerJoin` subuser)) = do
E.on $ submission E.?. SubmissionId E.==. subuser E.?. SubmissionUserSubmission E.on $ submission E.?. SubmissionId E.==. subuser E.?. SubmissionUserSubmission
E.&&. (E.just $ E.val uid) E.==. subuser E.?. SubmissionUserUser E.&&. E.just (E.val uid) E.==. subuser E.?. SubmissionUserUser
E.on $ submission E.?. SubmissionSheet E.==. E.just(sheet E.^. SheetId) E.on $ submission E.?. SubmissionSheet E.==. E.just(sheet E.^. SheetId)
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
@ -164,14 +164,14 @@ homeUser uid = do
anchorCell (CSheetR tid ssh csh shn SShowR) (toWidget $ display shn) anchorCell (CSheetR tid ssh csh shn SShowR) (toWidget $ display shn)
, sortable (Just "deadline") (i18nCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, _, E.Value deadline, _) } -> , sortable (Just "deadline") (i18nCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, _, E.Value deadline, _) } ->
cell $ formatTime SelFormatDateTime deadline >>= toWidget cell $ formatTime SelFormatDateTime deadline >>= toWidget
, sortable (Just "done") (i18nCell MsgDone) $ \(DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, E.Value mbsid) }) -> , sortable (Just "done") (i18nCell MsgDone) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, E.Value mbsid) } ->
case mbsid of case mbsid of
Nothing -> mempty Nothing -> mempty
(Just sid) -> anchorCellM (CSubmissionR tid ssh csh shn <$> encrypt sid <*> pure SubShowR) (Just sid) -> anchorCellM (CSubmissionR tid ssh csh shn <$> encrypt sid <*> pure SubShowR)
tickmark tickmark
] ]
let validator = def & defaultSorting [("done",SortDesc), ("deadline",SortDesc)] let validator = def & defaultSorting [("done",SortDesc), ("deadline",SortDesc)]
((), sheetTable) <- dbTable validator $ DBTable ((), sheetTable) <- dbTable validator DBTable
{ dbtSQLQuery = tableData { dbtSQLQuery = tableData
, dbtColonnade = colonnade , dbtColonnade = colonnade
, dbtProj = \row@DBRow{ dbrOutput = (E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, _) } , dbtProj = \row@DBRow{ dbrOutput = (E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, _) }
@ -206,7 +206,7 @@ homeUser uid = do
, dbtIdent = "upcomingdeadlines" :: Text , dbtIdent = "upcomingdeadlines" :: Text
} }
-- addMessage Warning "Vorabversion! Die Implementierung von Uni2work ist noch nicht abgeschlossen." -- addMessage Warning "Vorabversion! Die Implementierung von Uni2work ist noch nicht abgeschlossen."
defaultLayout $ do defaultLayout $
-- setTitle "Willkommen zum Uni2work Test!" -- setTitle "Willkommen zum Uni2work Test!"
$(widgetFile "homeUser") $(widgetFile "homeUser")
-- $(widgetFile "dsgvDisclaimer") -- $(widgetFile "dsgvDisclaimer")
@ -276,12 +276,14 @@ postHelpR = do
((res,formWidget),formEnctype) <- runFormPost $ renderAForm FormStandard $ helpForm mReferer mUid ((res,formWidget),formEnctype) <- runFormPost $ renderAForm FormStandard $ helpForm mReferer mUid
case res of case res of
FormSuccess (HelpForm{..}) -> do FormSuccess HelpForm{..} -> do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
queueJob' $ JobHelpRequest { jSender = hfUserId queueJob' JobHelpRequest
, jHelpRequest = hfRequest { jSender = hfUserId
, jRequestTime = now , jHelpRequest = hfRequest
, jReferer = hfReferer } , jRequestTime = now
, jReferer = hfReferer
}
-- redirect $ HelpR -- redirect $ HelpR
addMessageI Success MsgHelpSent addMessageI Success MsgHelpSent
return () return ()

View File

@ -67,7 +67,7 @@ getProfileR, postProfileR :: Handler Html
getProfileR = postProfileR getProfileR = postProfileR
postProfileR = do postProfileR = do
(uid, User{..}) <- requireAuthPair (uid, User{..}) <- requireAuthPair
let settingsTemplate = Just $ SettingsForm let settingsTemplate = Just SettingsForm
{ stgMaxFavourties = userMaxFavourites { stgMaxFavourties = userMaxFavourites
, stgTheme = userTheme , stgTheme = userTheme
, stgDateTime = userDateTimeFormat , stgDateTime = userDateTimeFormat
@ -92,13 +92,13 @@ postProfileR = do
-- prune Favourites to user-defined size -- prune Favourites to user-defined size
oldFavs <- selectKeysList [ CourseFavouriteUser ==. uid] oldFavs <- selectKeysList [ CourseFavouriteUser ==. uid]
[ Desc CourseFavouriteTime [ Desc CourseFavouriteTime
, OffsetBy $ stgMaxFavourties , OffsetBy stgMaxFavourties
] ]
mapM_ delete oldFavs mapM_ delete oldFavs
addMessageI Info $ MsgSettingsUpdate addMessageI Info MsgSettingsUpdate
redirect ProfileR -- TODO: them change does not happen without redirect redirect ProfileR -- TODO: them change does not happen without redirect
(FormFailure msgs) -> forM_ msgs $ (addMessage Warning) . toHtml (FormFailure msgs) -> forM_ msgs $ addMessage Warning . toHtml
_ -> return () _ -> return ()
let formText = Nothing :: Maybe UniWorXMessage let formText = Nothing :: Maybe UniWorXMessage
@ -109,7 +109,7 @@ postProfileR = do
postProfileDataR :: Handler Html postProfileDataR :: Handler Html
postProfileDataR = do postProfileDataR = do
((btnResult,_), _) <- runFormPost $ buttonForm ((btnResult,_), _) <- runFormPost buttonForm
case btnResult of case btnResult of
(FormSuccess BtnDelete) -> do (FormSuccess BtnDelete) -> do
(uid, User{..}) <- requireAuthPair (uid, User{..}) <- requireAuthPair
@ -119,7 +119,7 @@ postProfileDataR = do
$(addMessageFile Success "templates/deletedUser.hamlet") -- USE THIS ONE $(addMessageFile Success "templates/deletedUser.hamlet") -- USE THIS ONE
-- addMessageI Success $ MsgDeleteUser deletedSubmissions -- addMessageI Success $ MsgDeleteUser deletedSubmissions
-- when (groupSubmissions > 0) $ addMessageI Info $ MsgDeleteUserGroupSubmissions groupSubmissions -- when (groupSubmissions > 0) $ addMessageI Info $ MsgDeleteUserGroupSubmissions groupSubmissions
defaultLayout $ do defaultLayout
$(widgetFile "deletedUser") $(widgetFile "deletedUser")
(FormSuccess BtnAbort ) -> do (FormSuccess BtnAbort ) -> do
@ -156,72 +156,76 @@ deleteUser duid = do
E.&&. subUsers E.^. SubmissionUserUser E.!=. E.val duid E.&&. subUsers E.^. SubmissionUserUser E.!=. E.val duid
return E.countRows return E.countRows
E.where_ $ suser E.^. SubmissionUserUser E.==. E.val duid E.where_ $ suser E.^. SubmissionUserUser E.==. E.val duid
E.&&. (whereBuddies numBuddies) E.&&. whereBuddies numBuddies
return $ submission E.^. SubmissionId return $ submission E.^. SubmissionId
getSubmissionFiles :: SubmissionId -> DB [E.Value (Key File)] getSubmissionFiles :: SubmissionId -> DB [E.Value (Key File)]
getSubmissionFiles subId = E.select $ E.from $ \file -> do getSubmissionFiles subId = E.select $ E.from $ \file -> do
E.where_ $ E.exists $ E.from $ \submissionFile -> do E.where_ $ E.exists $ E.from $ \submissionFile ->
E.where_ $ submissionFile E.^. SubmissionFileSubmission E.==. E.val subId E.where_ $ submissionFile E.^. SubmissionFileSubmission E.==. E.val subId
E.&&. submissionFile E.^. SubmissionFileFile E.==. file E.^. FileId E.&&. submissionFile E.^. SubmissionFileFile E.==. file E.^. FileId
return $ file E.^. FileId return $ file E.^. FileId
deleteSingleSubmissionGroups = E.deleteCount $ E.from $ \submissionGroup -> do deleteSingleSubmissionGroups = E.deleteCount $ E.from $ \submissionGroup -> do
E.where_ $ E.exists $ E.from $ \subGroupUser -> do E.where_ $ E.exists $ E.from $ \subGroupUser ->
E.where_ $ subGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId E.where_ $ subGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.&&. subGroupUser E.^. SubmissionGroupUserUser E.==. E.val duid E.&&. subGroupUser E.^. SubmissionGroupUserUser E.==. E.val duid
E.where_ $ E.notExists $ E.from $ \subGroupUser -> do E.where_ $ E.notExists $ E.from $ \subGroupUser ->
E.where_ $ subGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId E.where_ $ subGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.&&. subGroupUser E.^. SubmissionGroupUserUser E.!=. E.val duid E.&&. subGroupUser E.^. SubmissionGroupUserUser E.!=. E.val duid
getProfileDataR :: Handler Html getProfileDataR :: Handler Html
getProfileDataR = do getProfileDataR = do
(uid, User{..}) <- requireAuthPair (uid, User{..}) <- requireAuthPair
-- mr <- getMessageRender -- mr <- getMessageRender
(admin_rights,lecturer_rights,lecture_corrector,studies) <- runDB $ (,,,) <$> (admin_rights,lecturer_rights,lecture_corrector,studies) <- runDB $ (,,,) <$>
(E.select $ E.from $ \(adright `E.InnerJoin` school) -> do E.select
E.where_ $ adright E.^. UserAdminUser E.==. E.val uid ( E.from $ \(adright `E.InnerJoin` school) -> do
E.on $ adright E.^. UserAdminSchool E.==. school E.^. SchoolId E.where_ $ adright E.^. UserAdminUser E.==. E.val uid
return (school E.^. SchoolShorthand) E.on $ adright E.^. UserAdminSchool E.==. school E.^. SchoolId
) return (school E.^. SchoolShorthand)
)
<*> <*>
(E.select $ E.from $ \(lecright `E.InnerJoin` school) -> do E.select
E.where_ $ lecright E.^. UserLecturerUser E.==. E.val uid ( E.from $ \(lecright `E.InnerJoin` school) -> do
E.on $ lecright E.^. UserLecturerSchool E.==. school E.^. SchoolId E.where_ $ lecright E.^. UserLecturerUser E.==. E.val uid
return (school E.^. SchoolShorthand) E.on $ lecright E.^. UserLecturerSchool E.==. school E.^. SchoolId
) return (school E.^. SchoolShorthand)
)
<*> <*>
(E.select $ E.distinct $ E.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do E.select
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId ( E.distinct $ E.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
return (course E.^. CourseTerm, course E.^. CourseSchool, course E.^. CourseShorthand) E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid
) return (course E.^. CourseTerm, course E.^. CourseSchool, course E.^. CourseShorthand)
)
<*> <*>
(E.select $ E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do E.select
E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid ( E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do
E.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid
E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId E.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId
return ( ( studydegree E.^. StudyDegreeName E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId
, studydegree E.^. StudyDegreeKey return ( ( studydegree E.^. StudyDegreeName
) , studydegree E.^. StudyDegreeKey
, ( studyterms E.^. StudyTermsName )
, studyterms E.^. StudyTermsKey , ( studyterms E.^. StudyTermsName
) , studyterms E.^. StudyTermsKey
, studyfeat E.^. StudyFeaturesType )
, studyfeat E.^. StudyFeaturesSemester) , studyfeat E.^. StudyFeaturesType
) , studyfeat E.^. StudyFeaturesSemester)
)
-- Tabelle mit eigenen Kursen -- Tabelle mit eigenen Kursen
(hasRows, ownedCoursesTable) <- mkOwnedCoursesTable uid (hasRows, ownedCoursesTable) <- mkOwnedCoursesTable uid
-- Tabelle mit allen Teilnehmer: Kurs (link), Datum -- Tabelle mit allen Teilnehmer: Kurs (link), Datum
enrolledCoursesTable <- mkEnrolledCoursesTable uid enrolledCoursesTable <- mkEnrolledCoursesTable uid
-- Tabelle mit allen Klausuren und Noten -- Tabelle mit allen Klausuren und Noten
examTable <- return [whamlet| Klausuren werden momentan leider noch nicht unterstützt.|] let examTable = [whamlet| Klausuren werden momentan leider noch nicht unterstützt.|]
-- Tabelle mit allen Abgaben und Abgabe-Gruppen -- Tabelle mit allen Abgaben und Abgabe-Gruppen
submissionTable <- mkSubmissionTable uid submissionTable <- mkSubmissionTable uid
-- Tabelle mit allen Abgabegruppen -- Tabelle mit allen Abgabegruppen
@ -229,42 +233,14 @@ getProfileDataR = do
-- Tabelle mit allen Korrektor-Aufgaben -- Tabelle mit allen Korrektor-Aufgaben
correctionsTable <- mkCorrectionsTable uid correctionsTable <- mkCorrectionsTable uid
-- Tabelle mit allen eigenen Tutorials -- Tabelle mit allen eigenen Tutorials
ownTutorialTable <- return [whamlet| Übungsgruppen werden momentan leider noch nicht unterstützt.|] let ownTutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|]
-- Tabelle mit allen Tutorials -- Tabelle mit allen Tutorials
tutorialTable <- return [whamlet| Übungsgruppen werden momentan leider noch nicht unterstützt.|] let tutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|]
-- Delete Button -- Delete Button
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form BtnDelete) (btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form BtnDelete)
-- TODO: move this into a Message and/or Widget-File -- TODO: move this into a Message and/or Widget-File
let delWdgt = [whamlet|
<form .form-inline method=post action=@{ProfileDataR} enctype=#{btnEnctype}>
<h2>
Sind Sie sich absolut sicher, alle Ihre in Uni2work gespeicherten Daten zu löschen?
<div .container>
Während der Testphase von Uni2work können Sie hiermit
Ihren Account bei Uni2work vollständig löschen.
Mit Ihrem Campus-Account können Sie sich aber danach
jederzeit erneut einloggen, wodurch wieder ein leerer Account erstellt wird.
<div .container>
Hochgeladene Hausaufgaben-Dateien werden unabhhängig vom Urherber nur dann gelöscht,
wenn die Dateien ausschließlich Ihnen zugeordnet sind.
Dateien aus Gruppenabgaben werden also erst dann gelöscht,
wenn alle Gruppenmitglieder Ihren Account gelöscht haben.
<div .container>
<em>Achtung:
Auch abgegebene Hausübungen werden gelöscht!
Falls ein Veranstalter Informationen darüber nicht anderweitig gespeichert hat,
kann dadurch ein etwaiger Hausaufgabenbonus verloren gehen.
(Verbuchte Noten sollten dadurch nicht betroffen sein, aber in einem etwaigen
Streitfall konnen die per Uni2work verwalteten Hausaufgaben dann
auch nicht mehr rekonstruiert/berücksichtigt werden.)
<div .container>
<em>Nach der Testphase von Uni2work wird das Löschen eines Accounts etwas
eingeschränkt werden, da z.B. Klausurnoten 5 Jahre bis nach Exmatrikulation
aufbewahrt werden müssen.
<div .container>
^{btnWdgt}
|]
defaultLayout $ do defaultLayout $ do
let delWdgt = $(widgetFile "widgets/data-delete")
$(widgetFile "profileData") $(widgetFile "profileData")
$(widgetFile "dsgvDisclaimer") $(widgetFile "dsgvDisclaimer")
@ -280,14 +256,14 @@ mkOwnedCoursesTable =
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Lecturer)) -> a) -> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Lecturer)) -> a)
withType = id withType = id
dbtSQLQuery' uid = \(course `E.InnerJoin` lecturer) -> do dbtSQLQuery' uid (course `E.InnerJoin` lecturer) = do
E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid
return ( course E.^. CourseTerm return ( course E.^. CourseTerm
, course E.^. CourseSchool , course E.^. CourseSchool
, course E.^. CourseShorthand , course E.^. CourseShorthand
) )
dbtProj = \x -> return $ x & _dbrOutput %~ (\(E.Value tid, E.Value ssh, E.Value csh) -> (tid,ssh,csh)) dbtProj = return . (_dbrOutput %~ (\(E.Value tid, E.Value ssh, E.Value csh) -> (tid,ssh,csh)))
dbtColonnade = mconcat dbtColonnade = mconcat
[ dbRow [ dbRow
@ -299,10 +275,10 @@ mkOwnedCoursesTable =
schoolCell <$> view (_dbrOutput . _1 . re _Just) schoolCell <$> view (_dbrOutput . _1 . re _Just)
<*> view (_dbrOutput . _2 ) <*> view (_dbrOutput . _2 )
, sortable (Just "course") (i18nCell MsgCourse) $ , sortable (Just "course") (i18nCell MsgCourse) $
courseCellCL <$> view (_dbrOutput) courseCellCL <$> view _dbrOutput
] ]
validator = def & defaultSorting [("term",SortDesc),("school",SortAsc),("course",SortAsc)] validator = def & defaultSorting [ ("term", SortDesc), ("school", SortAsc), ("course", SortAsc) ]
dbtSorting = Map.fromList dbtSorting = Map.fromList
[ ( "course", SortColumn $ withType $ \(crse `E.InnerJoin` _) -> crse E.^. CourseShorthand) [ ( "course", SortColumn $ withType $ \(crse `E.InnerJoin` _) -> crse E.^. CourseShorthand)
, ( "term" , SortColumn $ withType $ \(crse `E.InnerJoin` _) -> crse E.^. CourseTerm ) , ( "term" , SortColumn $ withType $ \(crse `E.InnerJoin` _) -> crse E.^. CourseTerm )
@ -313,7 +289,7 @@ mkOwnedCoursesTable =
, ( "term", FilterColumn $ withType $ \(crse `E.InnerJoin` _) -> emptyOrIn $ crse E.^. CourseTerm ) , ( "term", FilterColumn $ withType $ \(crse `E.InnerJoin` _) -> emptyOrIn $ crse E.^. CourseTerm )
, ( "school", FilterColumn $ withType $ \(crse `E.InnerJoin` _) -> emptyOrIn $ crse E.^. CourseSchool ) , ( "school", FilterColumn $ withType $ \(crse `E.InnerJoin` _) -> emptyOrIn $ crse E.^. CourseSchool )
] ]
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in (_1 %~ getAny) <$> (dbTableWidget validator DBTable{..}) in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..}
@ -340,7 +316,7 @@ mkEnrolledCoursesTable =
termCell <$> view (_dbrOutput . _1 . _entityVal . _courseTerm) termCell <$> view (_dbrOutput . _1 . _entityVal . _courseTerm)
, sortable (Just "school") (i18nCell MsgCourseSchool) . magnify (_dbrOutput . _1 . _entityVal) $ , sortable (Just "school") (i18nCell MsgCourseSchool) . magnify (_dbrOutput . _1 . _entityVal) $
schoolCell <$> view ( _courseTerm . re _Just) schoolCell <$> view ( _courseTerm . re _Just)
<*> view ( _courseSchool ) <*> view _courseSchool
, sortable (Just "course") (i18nCell MsgCourse) $ , sortable (Just "course") (i18nCell MsgCourse) $
courseCell <$> view (_dbrOutput . _1 . _entityVal) courseCell <$> view (_dbrOutput . _1 . _entityVal)
, sortable (Just "time") (i18nCell MsgRegistered) $ do , sortable (Just "time") (i18nCell MsgRegistered) $ do
@ -374,17 +350,16 @@ mkSubmissionTable =
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity Submission)`E.InnerJoin` E.SqlExpr (Entity SubmissionUser) )->a) -> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity Submission)`E.InnerJoin` E.SqlExpr (Entity SubmissionUser) )->a)
withType = id withType = id
dbtSQLQuery' uid = \(course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` subUser) -> do dbtSQLQuery' uid (course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` subUser) = do
E.on $ submission E.^. SubmissionId E.==. subUser E.^. SubmissionUserSubmission E.on $ submission E.^. SubmissionId E.==. subUser E.^. SubmissionUserSubmission
E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.where_ $ subUser E.^. SubmissionUserUser E.==. E.val uid E.where_ $ subUser E.^. SubmissionUserUser E.==. E.val uid
let crse = ( course E.^. CourseTerm let crse = ( course E.^. CourseTerm
, course E.^. CourseSchool , course E.^. CourseSchool
, course E.^. CourseShorthand , course E.^. CourseShorthand
) )
let sht = ( sheet E.^. SheetName let sht = sheet E.^. SheetName
)
return (crse, sht, submission, lastSubEdit uid submission) return (crse, sht, submission, lastSubEdit uid submission)
lastSubEdit uid submission = -- latest Edit-Time of this user for submission lastSubEdit uid submission = -- latest Edit-Time of this user for submission
@ -393,7 +368,7 @@ mkSubmissionTable =
E.&&. subEdit E.^. SubmissionEditUser E.==. E.val uid E.&&. subEdit E.^. SubmissionEditUser E.==. E.val uid
return . E.max_ $ subEdit E.^. SubmissionEditTime return . E.max_ $ subEdit E.^. SubmissionEditTime
dbtProj = \x -> return $ x dbtProj x = return $ x
& _dbrOutput . _1 %~ (\(E.Value tid, E.Value ssh, E.Value csh) -> (tid,ssh,csh)) & _dbrOutput . _1 %~ (\(E.Value tid, E.Value ssh, E.Value csh) -> (tid,ssh,csh))
& _dbrOutput . _2 %~ E.unValue & _dbrOutput . _2 %~ E.unValue
& _dbrOutput . _4 %~ E.unValue & _dbrOutput . _4 %~ E.unValue
@ -404,7 +379,7 @@ mkSubmissionTable =
termCell <$> view (_dbrOutput . _1 . _1) termCell <$> view (_dbrOutput . _1 . _1)
, sortable (Just "school") (i18nCell MsgCourseSchool) . magnify (_dbrOutput . _1 ) $ , sortable (Just "school") (i18nCell MsgCourseSchool) . magnify (_dbrOutput . _1 ) $
schoolCell <$> view ( _1. re _Just) schoolCell <$> view ( _1. re _Just)
<*> view ( _2 ) <*> view _2
, sortable (Just "course") (i18nCell MsgCourse) $ , sortable (Just "course") (i18nCell MsgCourse) $
courseCellCL <$> view (_dbrOutput . _1) courseCellCL <$> view (_dbrOutput . _1)
, sortable (Just "sheet") (i18nCell MsgSheet) . magnify _dbrOutput $ , sortable (Just "sheet") (i18nCell MsgSheet) . magnify _dbrOutput $
@ -439,7 +414,7 @@ mkSubmissionTable =
] ]
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
dbtSorting = dbtSorting' uid dbtSorting = dbtSorting' uid
in dbTableWidget' validator $ DBTable {..} in dbTableWidget' validator DBTable{..}
-- in do dbtSQLQuery <- dbtSQLQuery' -- in do dbtSQLQuery <- dbtSQLQuery'
-- dbtSorting <- dbtSorting' -- dbtSorting <- dbtSorting'
-- return $ dbTableWidget' validator $ DBTable {..} -- return $ dbTableWidget' validator $ DBTable {..}
@ -455,7 +430,7 @@ mkSubmissionGroupTable =
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity SubmissionGroup) `E.InnerJoin` E.SqlExpr (Entity SubmissionGroupUser) )->a) -> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity SubmissionGroup) `E.InnerJoin` E.SqlExpr (Entity SubmissionGroupUser) )->a)
withType = id withType = id
dbtSQLQuery' uid = \(course `E.InnerJoin` sgroup `E.InnerJoin` sguser) -> do dbtSQLQuery' uid (course `E.InnerJoin` sgroup `E.InnerJoin` sguser) = do
E.on $ sguser E.^. SubmissionGroupUserSubmissionGroup E.==. sgroup E.^. SubmissionGroupId E.on $ sguser E.^. SubmissionGroupUserSubmissionGroup E.==. sgroup E.^. SubmissionGroupId
E.on $ sgroup E.^. SubmissionGroupCourse E.==. course E.^. CourseId E.on $ sgroup E.^. SubmissionGroupCourse E.==. course E.^. CourseId
E.where_ $ sguser E.^. SubmissionGroupUserUser E.==. E.val uid E.where_ $ sguser E.^. SubmissionGroupUserUser E.==. E.val uid
@ -471,7 +446,7 @@ mkSubmissionGroupTable =
E.where_ $ sgEdit E.^. SubmissionGroupEditSubmissionGroup E.==. sgroup E.^. SubmissionGroupId E.where_ $ sgEdit E.^. SubmissionGroupEditSubmissionGroup E.==. sgroup E.^. SubmissionGroupId
return . E.max_ $ sgEdit E.^. SubmissionGroupEditTime return . E.max_ $ sgEdit E.^. SubmissionGroupEditTime
dbtProj = \x -> return $ x dbtProj x = return $ x
& _dbrOutput . _1 %~ (\(E.Value tid, E.Value ssh, E.Value csh) -> (tid,ssh,csh)) & _dbrOutput . _1 %~ (\(E.Value tid, E.Value ssh, E.Value csh) -> (tid,ssh,csh))
& _dbrOutput . _3 %~ E.unValue & _dbrOutput . _3 %~ E.unValue
@ -481,7 +456,7 @@ mkSubmissionGroupTable =
termCell <$> view (_dbrOutput . _1 . _1) termCell <$> view (_dbrOutput . _1 . _1)
, sortable (Just "school") (i18nCell MsgCourseSchool) . magnify (_dbrOutput . _1 ) $ , sortable (Just "school") (i18nCell MsgCourseSchool) . magnify (_dbrOutput . _1 ) $
schoolCell <$> view ( _1. re _Just) schoolCell <$> view ( _1. re _Just)
<*> view ( _2 ) <*> view _2
, sortable (Just "course") (i18nCell MsgCourse) $ , sortable (Just "course") (i18nCell MsgCourse) $
courseCellCL <$> view (_dbrOutput . _1) courseCellCL <$> view (_dbrOutput . _1)
, sortable (Just "submissiongroup") (i18nCell MsgSubmissionGroupName) . magnify (_dbrOutput . _2 . _entityVal) $ , sortable (Just "submissiongroup") (i18nCell MsgSubmissionGroupName) . magnify (_dbrOutput . _2 . _entityVal) $
@ -507,7 +482,7 @@ mkSubmissionGroupTable =
, ( "school", FilterColumn $ withType $ \(crse `E.InnerJoin` _ `E.InnerJoin` _) -> emptyOrIn $ crse E.^. CourseSchool ) , ( "school", FilterColumn $ withType $ \(crse `E.InnerJoin` _ `E.InnerJoin` _) -> emptyOrIn $ crse E.^. CourseSchool )
] ]
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
in dbTableWidget' validator $ DBTable {..} in dbTableWidget' validator DBTable{..}
@ -524,15 +499,15 @@ mkCorrectionsTable =
corrsAssigned uid sheet = E.sub_select . E.from $ \submission -> do corrsAssigned uid sheet = E.sub_select . E.from $ \submission -> do
E.where_ $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId E.where_ $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId
E.&&. submission E.^. SubmissionRatingBy E.==. E.just (E.val uid) E.&&. submission E.^. SubmissionRatingBy E.==. E.just (E.val uid)
return $ E.countRows return E.countRows
corrsCorrected uid sheet = E.sub_select . E.from $ \submission -> do corrsCorrected uid sheet = E.sub_select . E.from $ \submission -> do
E.where_ $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId E.where_ $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId
E.&&. submission E.^. SubmissionRatingBy E.==. E.just (E.val uid) E.&&. submission E.^. SubmissionRatingBy E.==. E.just (E.val uid)
E.&&. (E.not_ $ E.isNothing $ submission E.^. SubmissionRatingTime) E.&&. E.not_ (E.isNothing $ submission E.^. SubmissionRatingTime)
return $ E.countRows return E.countRows
dbtSQLQuery' uid = \(course `E.InnerJoin` sheet `E.InnerJoin` corrector) -> do dbtSQLQuery' uid (course `E.InnerJoin` sheet `E.InnerJoin` corrector) = do
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid
@ -542,7 +517,7 @@ mkCorrectionsTable =
) )
return (crse, sheet E.^. SheetName, corrector, (corrsAssigned uid sheet, corrsCorrected uid sheet)) return (crse, sheet E.^. SheetName, corrector, (corrsAssigned uid sheet, corrsCorrected uid sheet))
dbtProj = \x -> return $ x dbtProj x = return $ x
& _dbrOutput . _1 %~ (\(E.Value tid, E.Value ssh, E.Value csh) -> (tid,ssh,csh)) & _dbrOutput . _1 %~ (\(E.Value tid, E.Value ssh, E.Value csh) -> (tid,ssh,csh))
& _dbrOutput . _2 %~ E.unValue & _dbrOutput . _2 %~ E.unValue
@ -580,5 +555,5 @@ mkCorrectionsTable =
, ( "course", FilterColumn $ withType $ \(crse `E.InnerJoin` _ `E.InnerJoin` _) -> emptyOrIn $ crse E.^. CourseShorthand) , ( "course", FilterColumn $ withType $ \(crse `E.InnerJoin` _ `E.InnerJoin` _) -> emptyOrIn $ crse E.^. CourseShorthand)
] ]
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
in dbTableWidget' validator $ DBTable {..} in dbTableWidget' validator DBTable{..}

View File

@ -2,37 +2,9 @@ module Handler.School where
import Import import Import
-- import Control.Lens
-- import Utils.Lens
-- import Utils.TH
-- import Handler.Utils
-- import Handler.Utils.Table.Cells
--
-- -- import Data.Time
-- import qualified Data.Text as T
-- import Data.Function ((&))
-- -- import Yesod.Form.Bootstrap3
--
-- import qualified Data.Set as Set
-- import qualified Data.Map as Map
--
-- import Colonnade hiding (fromMaybe,bool)
--
-- import qualified Database.Esqueleto as E
--
-- import qualified Data.UUID.Cryptographic as UUID
getSchoolListR :: Handler Html getSchoolListR :: Handler Html
getSchoolListR = do getSchoolListR = error "getSchoolListR: Not implemented"
-- muid <- maybeAuthId
defaultLayout $ do
[whamlet|TODO: Liste aller Institute |] -- TODO
getSchoolShowR :: SchoolId -> Handler Html getSchoolShowR :: SchoolId -> Handler Html
getSchoolShowR ssh = do -- TODO getSchoolShowR = error "getSchoolShowR: Not implemented"
-- muid <- maybeAuthId
defaultLayout $ do
[whamlet|TODO: Informationen zu einem Institut |] -- TODO

View File

@ -31,7 +31,7 @@ import Control.Monad.Writer (MonadWriter(..), execWriterT)
-- import qualified Data.List as List -- import qualified Data.List as List
import Control.Monad.Trans.Except (ExceptT(..), runExceptT, mapExceptT, throwE) import Control.Monad.Trans.Except (runExceptT, mapExceptT, throwE)
import Network.Mime import Network.Mime
@ -39,8 +39,7 @@ import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Map (Map, (!), (!?)) import Data.Map (Map, (!?))
import qualified Data.Map as Map
import Data.Monoid (Sum(..), Any(..)) import Data.Monoid (Sum(..), Any(..))
@ -54,10 +53,6 @@ import Control.Monad.Random.Class (MonadRandom(..))
import Utils.Sql import Utils.Sql
instance Eq (Unique Sheet) where
(CourseSheet cid1 name1) == (CourseSheet cid2 name2) =
cid1 == cid2 && name1 == name2
{- {-
* Implement Handlers * Implement Handlers
* Implement Breadcrumbs in Foundation * Implement Breadcrumbs in Foundation
@ -183,8 +178,8 @@ getSheetListR tid ssh csh = do
(Just (Entity sid Submission{..})) -> (Just (Entity sid Submission{..})) ->
let mkCid = encrypt sid let mkCid = encrypt sid
mkRoute = do mkRoute = do
cid <- mkCid cid' <- mkCid
return $ CSubmissionR tid ssh csh sheetName cid CorrectionR return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR
protoCell = anchorCellM mkRoute $(widgetFile "widgets/rating") protoCell = anchorCellM mkRoute $(widgetFile "widgets/rating")
in protoCell & cellContents %~ (<* tell (sheetTypeSum sheetType submissionRatingPoints)) in protoCell & cellContents %~ (<* tell (sheetTypeSum sheetType submissionRatingPoints))
, sortable Nothing -- (Just "percent") , sortable Nothing -- (Just "percent")
@ -192,10 +187,11 @@ getSheetListR tid ssh csh = do
$ \(Entity _ Sheet{sheetType=sType}, _, mbSub) -> case mbSub of $ \(Entity _ Sheet{sheetType=sType}, _, mbSub) -> case mbSub of
(Just (Entity _ Submission{submissionRatingPoints=Just sPoints})) -> (Just (Entity _ Submission{submissionRatingPoints=Just sPoints})) ->
case preview (_grading . _maxPoints) sType of case preview (_grading . _maxPoints) sType of
(Nothing) -> mempty Just maxPoints
(Just maxPoints) -> | maxPoints /= 0 ->
let percent = sPoints / maxPoints let percent = sPoints / maxPoints
in textCell $ textPercent $ realToFrac percent in textCell $ textPercent $ realToFrac percent
_other -> mempty
_other -> mempty _other -> mempty
] ]
psValidator = def psValidator = def
@ -213,7 +209,7 @@ getSheetListR tid ssh csh = do
E.&&. submissionUser E.?. SubmissionUserUser E.==. E.val muid E.&&. submissionUser E.?. SubmissionUserUser E.==. E.val muid
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
return (sheet E.^. SheetType, submission E.?. SubmissionRatingPoints) return (sheet E.^. SheetType, submission E.?. SubmissionRatingPoints)
return $ foldMap (\(E.Value st, E.Value mbPts) -> sheetTypeSum st (join mbPts)) rows return $ foldMap (\(E.Value sheetType, E.Value mbPts) -> sheetTypeSum sheetType (join mbPts)) rows
(_, table) <- dbTable psValidator $ DBTable (_, table) <- dbTable psValidator $ DBTable
-- END ISSUE #223 -- END ISSUE #223
----------------------------------------------------- -----------------------------------------------------
@ -235,7 +231,7 @@ getSheetListR tid ssh csh = do
, SortColumn $ \(sheet `E.LeftOuterJoin` _) -> sheet E.^. SheetActiveTo , SortColumn $ \(sheet `E.LeftOuterJoin` _) -> sheet E.^. SheetActiveTo
) )
, ( "rating" , ( "rating"
, SortColumn $ \(_ `E.LeftOuterJoin` (submission `E.InnerJoin` submissionUser)) -> submission E.?. SubmissionRatingPoints , SortColumn $ \(_sheet `E.LeftOuterJoin` (submission `E.InnerJoin` _submissionUser)) -> submission E.?. SubmissionRatingPoints
) )
-- GitLab Issue $143: HOW TO SORT? -- GitLab Issue $143: HOW TO SORT?
-- , ( "percent" -- , ( "percent"
@ -271,9 +267,7 @@ instance Button UniWorX ButtonGeneratePseudonym where
-- Show single sheet -- Show single sheet
getSShowR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html getSShowR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
getSShowR tid ssh csh shn = do getSShowR tid ssh csh shn = do
entSheet <- runDB $ fetchSheet tid ssh csh shn Entity sid sheet <- runDB $ fetchSheet tid ssh csh shn
let sheet = entityVal entSheet
sid = entityKey entSheet
-- without Colonnade -- without Colonnade
-- fileNameTypes <- runDB $ E.select $ E.from $ -- fileNameTypes <- runDB $ E.select $ E.from $
-- \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> do -- \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> do
@ -287,19 +281,20 @@ getSShowR tid ssh csh shn = do
-- let fileLinks = map (\(E.Value fName, E.Value modified, E.Value fType) -> (CSheetR tid ssh csh (SheetFileR shn fType fName),modified)) fileNameTypes -- let fileLinks = map (\(E.Value fName, E.Value modified, E.Value fType) -> (CSheetR tid ssh csh (SheetFileR shn fType fName),modified)) fileNameTypes
-- with Colonnade -- with Colonnade
let fileData (sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) = do let fileData (sheet' `E.InnerJoin` sheetFile `E.InnerJoin` file) = do
-- Restrict to consistent rows that correspond to each other -- Restrict to consistent rows that correspond to each other
E.on (file E.^. FileId E.==. sheetFile E.^. SheetFileFile) E.on (file E.^. FileId E.==. sheetFile E.^. SheetFileFile)
E.on (sheetFile E.^. SheetFileSheet E.==. sheet E.^. SheetId) E.on (sheetFile E.^. SheetFileSheet E.==. sheet' E.^. SheetId)
-- filter to requested file -- filter to requested file
E.where_ $ sheet E.^. SheetId E.==. E.val sid E.where_ $ sheet' E.^. SheetId E.==. E.val sid
E.&&. E.not_ (E.isNothing $ file E.^. FileContent) E.&&. E.not_ (E.isNothing $ file E.^. FileContent)
-- return desired columns -- return desired columns
return $ (file E.^. FileTitle, file E.^. FileModified, sheetFile E.^. SheetFileType) return $ (file E.^. FileTitle, file E.^. FileModified, sheetFile E.^. SheetFileType)
let colonnadeFiles = widgetColonnade $ mconcat let colonnadeFiles = widgetColonnade $ mconcat
[ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> i18nCell ftype & cellContents %~ (\act -> act <* tell (Any True)) [ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> i18nCell ftype & cellContents %~ (\act -> act <* tell (Any True))
, sortable (Just "path") "Dateiname" $ anchorCell' (\(E.Value fName,_,E.Value fType) -> CSheetR tid ssh csh shn (SFileR fType fName)) , sortable (Just "path") "Dateiname" $ \(E.Value fName,_,E.Value fType) -> anchorCell
(\(E.Value fName,_,_) -> str2widget fName) (CSheetR tid ssh csh shn (SFileR fType fName))
(str2widget fName)
, sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> cell $ formatTime SelFormatDateTime (modified :: UTCTime) >>= toWidget , sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> cell $ formatTime SelFormatDateTime (modified :: UTCTime) >>= toWidget
] ]
let psValidator = def let psValidator = def
@ -314,13 +309,13 @@ getSShowR tid ssh csh shn = do
, dbtIdent = "files" :: Text , dbtIdent = "files" :: Text
, dbtSorting = Map.fromList , dbtSorting = Map.fromList
[ ( "type" [ ( "type"
, SortColumn $ \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> sheetFile E.^. SheetFileType , SortColumn $ \(_sheet `E.InnerJoin` sheetFile `E.InnerJoin` _file) -> sheetFile E.^. SheetFileType
) )
, ( "path" , ( "path"
, SortColumn $ \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> file E.^. FileTitle , SortColumn $ \(_sheet `E.InnerJoin` _sheetFile `E.InnerJoin` file) -> file E.^. FileTitle
) )
, ( "time" , ( "time"
, SortColumn $ \(sheet `E.InnerJoin` sheetFile `E.InnerJoin` file) -> file E.^. FileModified , SortColumn $ \(_sheet `E.InnerJoin` _sheetFile `E.InnerJoin` file) -> file E.^. FileModified
) )
] ]
} }
@ -346,7 +341,7 @@ getSShowR tid ssh csh shn = do
solutionFrom <- traverse (formatTime SelFormatDateTime) $ sheetSolutionFrom sheet solutionFrom <- traverse (formatTime SelFormatDateTime) $ sheetSolutionFrom sheet
$(widgetFile "sheetShow") $(widgetFile "sheetShow")
postSPseudonymR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent getSPseudonymR, postSPseudonymR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent
getSPseudonymR = postSPseudonymR getSPseudonymR = postSPseudonymR
postSPseudonymR tid ssh csh shn = do postSPseudonymR tid ssh csh shn = do
uid <- requireAuthId uid <- requireAuthId
@ -390,7 +385,6 @@ getSFileR tid ssh csh shn typ title = do
) )
-- return desired columns -- return desired columns
return $ (file E.^. FileTitle, file E.^. FileContent) return $ (file E.^. FileTitle, file E.^. FileContent)
let mimeType = defaultMimeLookup $ pack title
case results of case results of
[(E.Value fileTitle, E.Value fileContent)] [(E.Value fileTitle, E.Value fileContent)]
| Just fileContent' <- fileContent -> do | Just fileContent' <- fileContent -> do
@ -443,12 +437,10 @@ postSheetNewR = getSheetNewR
getSEditR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html getSEditR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
getSEditR tid ssh csh shn = do getSEditR tid ssh csh shn = do
(sheetEnt, sheetFileIds) <- runDB $ do (Entity sid Sheet{..}, sheetFileIds) <- runDB $ do
ent <- fetchSheet tid ssh csh shn ent <- fetchSheet tid ssh csh shn
fti <- getFtIdMap $ entityKey ent fti <- getFtIdMap $ entityKey ent
return (ent, fti) return (ent, fti)
let sid = entityKey sheetEnt
let oldSheet@(Sheet {..}) = entityVal sheetEnt
let template = Just $ SheetForm let template = Just $ SheetForm
{ sfName = sheetName { sfName = sheetName
, sfDescription = sheetDescription , sfDescription = sheetDescription
@ -547,7 +539,6 @@ getSDelR tid ssh csh shn = do
submissionno <- runDB $ do submissionno <- runDB $ do
sid <- fetchSheetId tid ssh csh shn sid <- fetchSheetId tid ssh csh shn
count [SubmissionSheet ==. sid] count [SubmissionSheet ==. sid]
let formTitle = MsgSheetDelHead tid ssh csh shn
let formText = Just $ MsgSheetDelText submissionno let formText = Just $ MsgSheetDelText submissionno
let actionUrl = CSheetR tid ssh csh shn SDelR let actionUrl = CSheetR tid ssh csh shn SDelR
defaultLayout $ do defaultLayout $ do
@ -622,7 +613,7 @@ correctorForm shid = do
let let
guardNonDeleted :: UserId -> Handler (Maybe UserId) guardNonDeleted :: UserId -> Handler (Maybe UserId)
guardNonDeleted uid = do guardNonDeleted uid = do
cID@CryptoID{..} <- encrypt uid :: Handler CryptoUUIDUser CryptoID{ciphertext} <- encrypt uid :: Handler CryptoUUIDUser
deleted <- lookupPostParam $ tshow ciphertext <> "-" <> "del" deleted <- lookupPostParam $ tshow ciphertext <> "-" <> "del"
return $ bool Just (const Nothing) (isJust deleted) uid return $ bool Just (const Nothing) (isJust deleted) uid
formCIDs <- mapM decrypt =<< catMaybes <$> liftHandlerT (map fromPathPiece <$> lookupPostParams cListIdent :: Handler [Maybe CryptoUUIDUser]) formCIDs <- mapM decrypt =<< catMaybes <$> liftHandlerT (map fromPathPiece <$> lookupPostParams cListIdent :: Handler [Maybe CryptoUUIDUser])
@ -644,7 +635,7 @@ correctorForm shid = do
let let
tutorField :: Field Handler [UserEmail] tutorField :: Field Handler [UserEmail]
tutorField = convertField (map CI.mk) (map CI.original) $ multiEmailField tutorField = convertField (map CI.mk) (map CI.original) $ multiEmailField
{ fieldView = \theId name attrs val isReq -> asWidgetT $ do { fieldView = \theId name attrs _val isReq -> asWidgetT $ do
listIdent <- newIdent listIdent <- newIdent
userId <- handlerToWidget requireAuthId userId <- handlerToWidget requireAuthId
previousCorrectors <- handlerToWidget . runDB . E.select . E.from $ \(user `E.InnerJoin` sheetCorrector `E.InnerJoin` sheet `E.InnerJoin` course `E.InnerJoin` lecturer) -> E.distinctOnOrderBy [E.asc $ user E.^. UserEmail ] $ do previousCorrectors <- handlerToWidget . runDB . E.select . E.from $ \(user `E.InnerJoin` sheetCorrector `E.InnerJoin` sheet `E.InnerJoin` course `E.InnerJoin` lecturer) -> E.distinctOnOrderBy [E.asc $ user E.^. UserEmail ] $ do
@ -684,7 +675,7 @@ correctorForm shid = do
let let
constructFields :: (UserId, Text, (CorrectorState, Load)) -> MForm Handler CorrectorForm constructFields :: (UserId, Text, (CorrectorState, Load)) -> MForm Handler CorrectorForm
constructFields (uid, uname, (state, Load{..})) = do constructFields (uid, uname, (state, Load{..})) = do
cID@CryptoID{..} <- encrypt uid :: MForm Handler CryptoUUIDUser CryptoID{ciphertext} <- encrypt uid :: MForm Handler CryptoUUIDUser
let let
fs name = "" fs name = ""
{ fsName = Just $ tshow ciphertext <> "-" <> name { fsName = Just $ tshow ciphertext <> "-" <> name
@ -739,7 +730,7 @@ correctorForm shid = do
{ fvLabel = text $ mr MsgCorrectors { fvLabel = text $ mr MsgCorrectors
, fvTooltip = Nothing , fvTooltip = Nothing
, fvId = "" , fvId = ""
, fvInput = Yesod.encodeCellTable tableDefault corrColonnade corrData >> mapM_ idField corrData >> mapM_ delField deletions , fvInput = Yesod.encodeCellTable tableDefault corrColonnade corrData >> mapM_ idField corrData >> mapM_ delField deletions'
, fvErrors = Nothing , fvErrors = Nothing
, fvRequired = True , fvRequired = True
} }
@ -764,9 +755,9 @@ getSCorrR tid ssh csh shn = do
case res of case res of
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
FormSuccess res -> runDB $ do FormSuccess res' -> runDB $ do
deleteWhere [SheetCorrectorSheet ==. shid] deleteWhere [SheetCorrectorSheet ==. shid]
insertMany_ $ Set.toList res insertMany_ $ Set.toList res'
addMessageI Success MsgCorrectorsUpdated addMessageI Success MsgCorrectorsUpdated
FormMissing -> return () FormMissing -> return ()

View File

@ -1,6 +1,6 @@
module Handler.Submission where module Handler.Submission where
import Import hiding (joinPath) import Import
import Jobs import Jobs
@ -19,7 +19,6 @@ import Network.Mime
import Data.Monoid (Any(..)) import Data.Monoid (Any(..))
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
-- import qualified Data.Maybe -- import qualified Data.Maybe
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
@ -51,11 +50,11 @@ import System.FilePath
makeSubmissionForm :: Maybe SubmissionId -> UploadMode -> SheetGroup -> [UserEmail] -> Form (Maybe (Source Handler File), [UserEmail]) makeSubmissionForm :: Maybe SubmissionId -> UploadMode -> SheetGroup -> [UserEmail] -> Form (Maybe (Source Handler File), [UserEmail])
makeSubmissionForm msmid uploadMode grouping buddies = identForm FIDsubmission $ \html -> do makeSubmissionForm msmid uploadMode grouping buddies = identForm FIDsubmission $ \html -> do
let let
fileUpload = case uploadMode of fileUploadForm = case uploadMode of
NoUpload -> pure Nothing NoUpload -> pure Nothing
(Upload unpackZips) -> (bool (\f fs _ -> Just <$> areq f fs Nothing) aopt $ isJust msmid) (zipFileField unpackZips) (fsm $ bool MsgSubmissionFile MsgSubmissionArchive unpackZips) Nothing (Upload unpackZips) -> (bool (\f fs _ -> Just <$> areq f fs Nothing) aopt $ isJust msmid) (zipFileField unpackZips) (fsm $ bool MsgSubmissionFile MsgSubmissionArchive unpackZips) Nothing
flip (renderAForm FormStandard) html $ (,) flip (renderAForm FormStandard) html $ (,)
<$> fileUpload <$> fileUploadForm
<*> (catMaybes <$> sequenceA [bool aforced' aopt editableBuddies ciField (fslpI (MsgSubmissionMember g) "user@campus.lmu.de" ) buddy <*> (catMaybes <$> sequenceA [bool aforced' aopt editableBuddies ciField (fslpI (MsgSubmissionMember g) "user@campus.lmu.de" ) buddy
| g <- [1..(max (fromIntegral groupNr) $ length buddies)] -- groupNr might have decreased meanwhile | g <- [1..(max (fromIntegral groupNr) $ length buddies)] -- groupNr might have decreased meanwhile
| buddy <- map (Just . Just) buddies ++ repeat Nothing -- show current buddies | buddy <- map (Just . Just) buddies ++ repeat Nothing -- show current buddies
@ -89,7 +88,7 @@ getSubmissionOwnR tid ssh csh shn = do
E.&&. submission E.^. SubmissionSheet E.==. E.val shid E.&&. submission E.^. SubmissionSheet E.==. E.val shid
return $ submission E.^. SubmissionId return $ submission E.^. SubmissionId
case submissions of case submissions of
((E.Value sid):_) -> return sid (E.Value sid : _) -> return sid
[] -> notFound [] -> notFound
cID <- encrypt sid cID <- encrypt sid
redirect $ CSubmissionR tid ssh csh shn cID SubShowR redirect $ CSubmissionR tid ssh csh shn cID SubShowR
@ -132,19 +131,21 @@ submissionHelper tid ssh csh shn (SubmissionMode mcid) = do
return (csheet, map E.unValue buddies, []) return (csheet, map E.unValue buddies, [])
(E.Value smid:_) -> do (E.Value smid:_) -> do
cID <- encrypt smid cID <- encrypt smid
addMessageI Info $ MsgSubmissionAlreadyExists addMessageI Info MsgSubmissionAlreadyExists
redirect $ CSubmissionR tid ssh csh shn cID SubShowR redirect $ CSubmissionR tid ssh csh shn cID SubShowR
(Just smid) -> do (Just smid) -> do
void $ submissionMatchesSheet tid ssh csh shn (fromJust mcid) void $ submissionMatchesSheet tid ssh csh shn (fromJust mcid)
shid' <- submissionSheet <$> get404 smid shid' <- submissionSheet <$> get404 smid
unless (shid == shid') $
invalidArgsI [MsgSubmissionWrongSheet]
-- fetch buddies from current submission -- fetch buddies from current submission
(Any isOwner, buddies) <- do (Any isOwner, buddies) <- do
submittors <- E.select . E.from $ \(submissionUser `E.InnerJoin` user) -> do submittors <- E.select . E.from $ \(submissionUser `E.InnerJoin` user) -> do
E.on (submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId) E.on (submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId)
E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val smid E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val smid
E.orderBy [E.asc $ user E.^. UserEmail] E.orderBy [E.asc $ user E.^. UserEmail]
return $ (user E.^. UserId, user E.^. UserEmail) return (user E.^. UserId, user E.^. UserEmail)
let breakUserFromBuddies (E.Value userID, E.Value email) let breakUserFromBuddies (E.Value userID, E.Value email)
| uid == userID = (Any True , []) | uid == userID = (Any True , [])
| otherwise = (Any False, [email]) | otherwise = (Any False, [email])
@ -159,17 +160,17 @@ submissionHelper tid ssh csh shn (SubmissionMode mcid) = do
let userName = if isOwner || maySubmit let userName = if isOwner || maySubmit
then E.just $ user E.^. UserDisplayName then E.just $ user E.^. UserDisplayName
else E.nothing else E.nothing
return $ (userName, submissionEdit E.^. SubmissionEditTime) return (userName, submissionEdit E.^. SubmissionEditTime)
forM raw $ \(E.Value name, E.Value time) -> (name, ) <$> formatTime SelFormatDateTime time forM raw $ \(E.Value name, E.Value time) -> (name, ) <$> formatTime SelFormatDateTime time
return (csheet,buddies,lastEdits) return (csheet,buddies,lastEdits)
((res,formWidget), formEnctype) <- runFormPost $ makeSubmissionForm msmid sheetUploadMode sheetGrouping buddies ((res,formWidget), formEnctype) <- runFormPost $ makeSubmissionForm msmid sheetUploadMode sheetGrouping buddies
mCID <- runDBJobs $ do mCID <- runDBJobs $ do
res' <- case res of res' <- case res of
(FormMissing ) -> return $ FormMissing FormMissing -> return FormMissing
(FormFailure failmsgs) -> return $ FormFailure failmsgs (FormFailure failmsgs) -> return $ FormFailure failmsgs
(FormSuccess (mFiles,[])) -> return $ FormSuccess (mFiles,[]) -- Type change (FormSuccess (mFiles,[])) -> return $ FormSuccess (mFiles,[]) -- Type change
(FormSuccess (mFiles,gEMails@(_:_))) -- Validate AdHoc Group Members (FormSuccess (mFiles,gEMails@(_:_))) -- Validate AdHoc Group Members
| (Arbitrary {..}) <- sheetGrouping -> do | Arbitrary{..} <- sheetGrouping -> do
-- , length gEMails < maxParticipants -> do -- < since submitting user is already accounted for -- , length gEMails < maxParticipants -> do -- < since submitting user is already accounted for
let prep :: [(E.Value UserEmail, (E.Value UserId, E.Value Bool, E.Value Bool))] -> Map (CI Text) (Maybe (UserId, Bool, Bool)) let prep :: [(E.Value UserEmail, (E.Value UserId, E.Value Bool, E.Value Bool))] -> Map (CI Text) (Maybe (UserId, Bool, Bool))
prep ps = Map.filter (maybe True $ \(i,_,_) -> i /= uid) . Map.fromList $ map (, Nothing) gEMails ++ [(m, Just (i,p,s))|(E.Value m, (E.Value i, E.Value p, E.Value s)) <- ps] prep ps = Map.filter (maybe True $ \(i,_,_) -> i /= uid) . Map.fromList $ map (, Nothing) gEMails ++ [(m, Just (i,p,s))|(E.Value m, (E.Value i, E.Value p, E.Value s)) <- ps]
@ -211,8 +212,7 @@ submissionHelper tid ssh csh shn (SubmissionMode mcid) = do
case res' of case res' of
(FormSuccess (mFiles,(setFromList -> adhocIds))) -> do (FormSuccess (mFiles, setFromList -> adhocIds)) -> do
now <- liftIO $ getCurrentTime
smid <- do smid <- do
smid <- case (mFiles, msmid) of smid <- case (mFiles, msmid) of
(Nothing, Just smid) -- no new files, existing submission partners updated (Nothing, Just smid) -- no new files, existing submission partners updated
@ -261,13 +261,13 @@ submissionHelper tid ssh csh shn (SubmissionMode mcid) = do
Just isFile = origIsFile <|> corrIsFile Just isFile = origIsFile <|> corrIsFile
in if in if
| Just True <- origIsFile -> anchorCell (CSubmissionR tid ssh csh shn cid $ SubDownloadR SubmissionOriginal fileTitle') | Just True <- origIsFile -> anchorCell (CSubmissionR tid ssh csh shn cid $ SubDownloadR SubmissionOriginal fileTitle')
([whamlet|#{fileTitle'}|]) [whamlet|#{fileTitle'}|]
| otherwise -> textCell $ bool (<> "/") id isFile fileTitle' | otherwise -> textCell $ bool (<> "/") id isFile fileTitle'
, sortable (toNothing "state") (i18nCell MsgCorState) $ \(coalesce -> (_, mCorr)) -> case mCorr of , sortable (toNothing "state") (i18nCell MsgCorState) $ \(coalesce -> (_, mCorr)) -> case mCorr of
Nothing -> cell mempty Nothing -> cell mempty
Just (_, Entity _ File{..}) Just (_, Entity _ File{..})
| isJust fileContent -> anchorCell (CSubmissionR tid ssh csh shn cid $ SubDownloadR SubmissionCorrected fileTitle) | isJust fileContent -> anchorCell (CSubmissionR tid ssh csh shn cid $ SubDownloadR SubmissionCorrected fileTitle)
([whamlet|_{MsgFileCorrected}|]) [whamlet|_{MsgFileCorrected}|]
| otherwise -> i18nCell MsgCorrected | otherwise -> i18nCell MsgCorrected
, sortable (Just "time") (i18nCell MsgFileModified) $ \(coalesce -> (mOrig, mCorr)) -> let , sortable (Just "time") (i18nCell MsgFileModified) $ \(coalesce -> (mOrig, mCorr)) -> let
origTime = fileModified . entityVal . snd <$> mOrig origTime = fileModified . entityVal . snd <$> mOrig
@ -298,10 +298,10 @@ submissionHelper tid ssh csh shn (SubmissionMode mcid) = do
, dbtIdent = "files" :: Text , dbtIdent = "files" :: Text
, dbtSorting = Map.fromList , dbtSorting = Map.fromList
[ ( "path" [ ( "path"
, SortColumn $ \((sf1 `E.InnerJoin` f1) `E.FullOuterJoin` (sf2 `E.InnerJoin` f2)) -> E.coalesce [f1 E.?. FileTitle, f2 E.?. FileTitle] , SortColumn $ \((_sf1 `E.InnerJoin` f1) `E.FullOuterJoin` (_sf2 `E.InnerJoin` f2)) -> E.coalesce [f1 E.?. FileTitle, f2 E.?. FileTitle]
) )
, ( "time" , ( "time"
, SortColumn $ \((sf1 `E.InnerJoin` f1) `E.FullOuterJoin` (sf2 `E.InnerJoin` f2)) -> (E.unsafeSqlFunction "GREATEST" ([f1 E.?. FileModified, f2 E.?. FileModified] :: [E.SqlExpr (E.Value (Maybe UTCTime))]) :: E.SqlExpr (E.Value (Maybe UTCTime))) , SortColumn $ \((_sf1 `E.InnerJoin` f1) `E.FullOuterJoin` (_sf2 `E.InnerJoin` f2)) -> (E.unsafeSqlFunction "GREATEST" ([f1 E.?. FileModified, f2 E.?. FileModified] :: [E.SqlExpr (E.Value (Maybe UTCTime))]) :: E.SqlExpr (E.Value (Maybe UTCTime)))
) )
] ]
, dbtFilter = Map.empty , dbtFilter = Map.empty
@ -316,41 +316,39 @@ submissionHelper tid ssh csh shn (SubmissionMode mcid) = do
getSubDownloadR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionFileType -> FilePath -> Handler TypedContent getSubDownloadR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> SubmissionFileType -> FilePath -> Handler TypedContent
getSubDownloadR tid ssh csh shn cID (submissionFileTypeIsUpdate -> isUpdate) path = do getSubDownloadR tid ssh csh shn cID (submissionFileTypeIsUpdate -> isUpdate) path = runDB $ do
runDB $ do submissionID <- submissionMatchesSheet tid ssh csh shn cID
submissionID <- submissionMatchesSheet tid ssh csh shn cID
isRating <- maybe False (== submissionID) <$> isRatingFile path isRating <- (== Just submissionID) <$> isRatingFile path
when (isUpdate || isRating) $ when (isUpdate || isRating) $
guardAuthResult =<< evalAccessDB (CSubmissionR tid ssh csh shn cID CorrectionR) False guardAuthResult =<< evalAccessDB (CSubmissionR tid ssh csh shn cID CorrectionR) False
case isRating of
True
| isUpdate -> do
file <- runMaybeT $ lift . ratingFile cID =<< MaybeT (getRating submissionID)
maybe notFound (return . toTypedContent . Text.decodeUtf8) $ fileContent =<< file
| otherwise -> notFound
False -> do
results <- E.select . E.from $ \(sf `E.InnerJoin` f) -> do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
E.&&. f E.^. FileTitle E.==. E.val path
E.&&. E.not_ (sf E.^. SubmissionFileIsDeletion)
E.&&. sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate
-- E.&&. E.not_ (E.isNothing $ f E.^. FileContent) -- This is fine, we just return 204
return f
let fileName = Text.pack $ takeFileName path case isRating of
case results of True
[Entity _ File{ fileContent = Just c, fileTitle }] -> do | isUpdate -> do
whenM downloadFiles $ file <- runMaybeT $ lift . ratingFile cID =<< MaybeT (getRating submissionID)
addHeader "Content-Disposition" [st|attachment; filename="#{takeFileName fileTitle}"|] maybe notFound (return . toTypedContent . Text.decodeUtf8) $ fileContent =<< file
return $ TypedContent (defaultMimeLookup (pack fileTitle) <> "; charset=utf-8") (toContent c) | otherwise -> notFound
[Entity _ File{ fileContent = Nothing }] -> sendResponseStatus noContent204 () False -> do
other -> do results <- E.select . E.from $ \(sf `E.InnerJoin` f) -> do
$logErrorS "SubDownloadR" $ "Multiple matching files: " <> tshow other E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
error "Multiple matching files found." E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
E.&&. f E.^. FileTitle E.==. E.val path
E.&&. E.not_ (sf E.^. SubmissionFileIsDeletion)
E.&&. sf E.^. SubmissionFileIsUpdate E.==. E.val isUpdate
-- E.&&. E.not_ (E.isNothing $ f E.^. FileContent) -- This is fine, we just return 204
return f
case results of
[Entity _ File{ fileContent = Just c, fileTitle }] -> do
whenM downloadFiles $
addHeader "Content-Disposition" [st|attachment; filename="#{takeFileName fileTitle}"|]
return $ TypedContent (defaultMimeLookup (pack fileTitle) <> "; charset=utf-8") (toContent c)
[Entity _ File{ fileContent = Nothing }] -> sendResponseStatus noContent204 ()
other -> do
$logErrorS "SubDownloadR" $ "Multiple matching files: " <> tshow other
error "Multiple matching files found."
getSubArchiveR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> ZIPArchiveName SubmissionFileType -> Handler TypedContent getSubArchiveR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> ZIPArchiveName SubmissionFileType -> Handler TypedContent
getSubArchiveR tid ssh csh shn cID (ZIPArchiveName sfType) = do getSubArchiveR tid ssh csh shn cID (ZIPArchiveName sfType) = do
@ -367,7 +365,7 @@ getSubArchiveR tid ssh csh shn cID (ZIPArchiveName sfType) = do
rating <- lift $ getRating submissionID rating <- lift $ getRating submissionID
let let
fileSource = case sfType of fileSelect = case sfType of
SubmissionOriginal -> E.selectSource . E.from $ \(sf `E.InnerJoin` f) -> do SubmissionOriginal -> E.selectSource . E.from $ \(sf `E.InnerJoin` f) -> do
E.on $ f E.^. FileId E.==. sf E.^. SubmissionFileFile E.on $ f E.^. FileId E.==. sf E.^. SubmissionFileFile
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
@ -376,7 +374,7 @@ getSubArchiveR tid ssh csh shn cID (ZIPArchiveName sfType) = do
_ -> submissionFileSource submissionID _ -> submissionFileSource submissionID
fileSource' = do fileSource' = do
fileSource .| Conduit.map entityVal fileSelect .| Conduit.map entityVal
when (sfType == SubmissionCorrected) $ when (sfType == SubmissionCorrected) $
maybe (return ()) (yieldM . ratingFile cID) rating maybe (return ()) (yieldM . ratingFile cID) rating

View File

@ -50,11 +50,12 @@ postMessageR cID = do
cID' <- encrypt tId cID' <- encrypt tId
runFormPost . identForm (FIDSystemMessageModifyTranslation $ ciphertext cID') . renderAForm FormStandard runFormPost . identForm (FIDSystemMessageModifyTranslation $ ciphertext cID') . renderAForm FormStandard
$ (,) $ (,)
<$> ( fmap (Entity tId) $ SystemMessageTranslation <$> fmap (Entity tId)
<$> pure systemMessageTranslationMessage ( SystemMessageTranslation
<*> areq (langField False) (fslpI MsgSystemMessageLanguage "RFC1766-Sprachcode") (Just systemMessageTranslationLanguage) <$> pure systemMessageTranslationMessage
<*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") (Just systemMessageTranslationContent) <*> areq (langField False) (fslpI MsgSystemMessageLanguage "RFC1766-Sprachcode") (Just systemMessageTranslationLanguage)
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") (Just systemMessageTranslationSummary) <*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") (Just systemMessageTranslationContent)
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") (Just systemMessageTranslationSummary)
) )
<*> combinedButtonField (universeF :: [BtnSubmitDelete]) <*> combinedButtonField (universeF :: [BtnSubmitDelete])
@ -68,25 +69,11 @@ postMessageR cID = do
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") Nothing <*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") Nothing
<* submitButton <* submitButton
formResult modifyRes $ \SystemMessage{..} -> do formResult modifyRes $ modifySystemMessage smId
runDB $ update smId
[ SystemMessageFrom =. systemMessageFrom
, SystemMessageTo =. systemMessageTo
, SystemMessageAuthenticatedOnly =. systemMessageAuthenticatedOnly
, SystemMessageSeverity =. systemMessageSeverity
, SystemMessageDefaultLanguage =. systemMessageDefaultLanguage
, SystemMessageContent =. systemMessageContent
, SystemMessageSummary =. systemMessageSummary
]
addMessageI Success MsgSystemMessageEditSuccess
redirect $ MessageR cID
formResult addTransRes $ \smt -> do formResult addTransRes addTranslation
runDB . void . insert $ smt
addMessageI Success MsgSystemMessageAddTranslationSuccess
redirect $ MessageR cID
forM_ modifyTranss . flip formResult $ \(Entity tId SystemMessageTranslation{..}, (catMaybes -> acts)) -> case acts of forM_ modifyTranss . flip formResult $ \(Entity tId SystemMessageTranslation{..}, catMaybes -> acts) -> case acts of
[BtnDelete'] -> do [BtnDelete'] -> do
runDB $ delete tId runDB $ delete tId
addMessageI Success MsgSystemMessageDeleteTranslationSuccess addMessageI Success MsgSystemMessageDeleteTranslationSuccess
@ -125,9 +112,26 @@ postMessageR cID = do
maySubmit <- (== Authorized) <$> evalAccess (MessageR cID) True maySubmit <- (== Authorized) <$> evalAccess (MessageR cID) True
forms <- traverse (const mkForm) $ () <$ guard maySubmit forms <- traverse (const mkForm) $ () <$ guard maySubmit
defaultLayout $ do defaultLayout
$(widgetFile "system-message") $(widgetFile "system-message")
where
modifySystemMessage smId SystemMessage{..} = do
runDB $ update smId
[ SystemMessageFrom =. systemMessageFrom
, SystemMessageTo =. systemMessageTo
, SystemMessageAuthenticatedOnly =. systemMessageAuthenticatedOnly
, SystemMessageSeverity =. systemMessageSeverity
, SystemMessageDefaultLanguage =. systemMessageDefaultLanguage
, SystemMessageContent =. systemMessageContent
, SystemMessageSummary =. systemMessageSummary
]
addMessageI Success MsgSystemMessageEditSuccess
redirect $ MessageR cID
addTranslation translation = do
runDB . void $ insert translation
addMessageI Success MsgSystemMessageAddTranslationSuccess
redirect $ MessageR cID
type MessageListData = DBRow (Entity SystemMessage, Maybe SystemMessageTranslation) type MessageListData = DBRow (Entity SystemMessage, Maybe SystemMessageTranslation)
@ -159,7 +163,7 @@ postMessageListR = do
dbtColonnade = mconcat dbtColonnade = mconcat
[ dbSelect id $ \DBRow{ dbrOutput = (Entity smId _, _) } -> encrypt smId [ dbSelect id $ \DBRow{ dbrOutput = (Entity smId _, _) } -> encrypt smId
, dbRow , dbRow
, sortable Nothing (i18nCell MsgSystemMessageId) $ \DBRow{ dbrOutput = (Entity smId _, _) } -> anchorCellM' (encrypt smId) (\cID -> MessageR cID) (toWidget . tshow . ciphertext) , sortable Nothing (i18nCell MsgSystemMessageId) $ \DBRow{ dbrOutput = (Entity smId _, _) } -> anchorCellM' (encrypt smId) MessageR (toWidget . tshow . ciphertext)
, sortable (Just "from") (i18nCell MsgSystemMessageFrom) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell $ maybe mempty (formatTimeW SelFormatDateTime) systemMessageFrom , sortable (Just "from") (i18nCell MsgSystemMessageFrom) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell $ maybe mempty (formatTimeW SelFormatDateTime) systemMessageFrom
, sortable (Just "to") (i18nCell MsgSystemMessageTo) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell $ maybe mempty (formatTimeW SelFormatDateTime) systemMessageTo , sortable (Just "to") (i18nCell MsgSystemMessageTo) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell $ maybe mempty (formatTimeW SelFormatDateTime) systemMessageTo
, sortable (Just "authenticated") (i18nCell MsgSystemMessageAuthenticatedOnly) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> tickmarkCell systemMessageAuthenticatedOnly , sortable (Just "authenticated") (i18nCell MsgSystemMessageAuthenticatedOnly) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> tickmarkCell systemMessageAuthenticatedOnly
@ -172,12 +176,12 @@ postMessageListR = do
] ]
dbtProj DBRow{ dbrOutput = smE@(Entity smId _), .. } = do dbtProj DBRow{ dbrOutput = smE@(Entity smId _), .. } = do
Just (_, smT) <- lift $ getSystemMessage appLanguages smId Just (_, smT) <- lift $ getSystemMessage appLanguages smId
return $ DBRow return DBRow
{ dbrOutput = (smE, smT) { dbrOutput = (smE, smT)
, .. , ..
} }
psValidator = def :: PSValidator (MForm (HandlerT UniWorX IO)) (FormResult (DBFormResult MessageListData CryptoUUIDSystemMessage Bool)) psValidator = def :: PSValidator (MForm (HandlerT UniWorX IO)) (FormResult (DBFormResult MessageListData CryptoUUIDSystemMessage Bool))
tableForm <- dbTable psValidator $ DBTable tableForm <- dbTable psValidator DBTable
{ dbtSQLQuery { dbtSQLQuery
, dbtColonnade , dbtColonnade
, dbtProj , dbtProj
@ -191,8 +195,8 @@ postMessageListR = do
, dbtIdent = "messages" :: Text , dbtIdent = "messages" :: Text
} }
((tableRes, tableView), tableEncoding) <- runFormPost . identForm FIDSystemMessageTable $ \csrf -> do ((tableRes, tableView), tableEncoding) <- runFormPost . identForm FIDSystemMessageTable $ \csrf -> do
((fmap (Map.keysSet . Map.filter id . getDBFormResult (const False)) -> selectionRes), table) <- tableForm csrf (fmap $ Map.keysSet . Map.filter id . getDBFormResult (const False) -> selectionRes, table) <- tableForm csrf
now <- liftIO $ getCurrentTime now <- liftIO getCurrentTime
let actions = Map.fromList let actions = Map.fromList
[ (SMDelete, pure SMDDelete) [ (SMDelete, pure SMDDelete)
, (SMActivate, SMDActivate <$> aopt utcTimeField (fslI MsgSystemMessageTimestamp) (Just $ Just now)) , (SMActivate, SMDActivate <$> aopt utcTimeField (fslI MsgSystemMessageTimestamp) (Just $ Just now))
@ -223,8 +227,8 @@ postMessageListR = do
runDB $ updateWhere [ SystemMessageId <-. selection' ] [ SystemMessageTo =. ts ] runDB $ updateWhere [ SystemMessageId <-. selection' ] [ SystemMessageTo =. ts ]
$(addMessageFile Success "templates/messages/systemMessagesSetTo.hamlet") $(addMessageFile Success "templates/messages/systemMessagesSetTo.hamlet")
redirect MessageListR redirect MessageListR
FormSuccess (_, selection) FormSuccess (_, _selection) -- prop> null _selection
| null selection -> addMessageI Error MsgSystemMessageEmptySelection -> addMessageI Error MsgSystemMessageEmptySelection
((addRes, addView), addEncoding) <- runFormPost . identForm FIDSystemMessageAdd . renderAForm FormStandard $ SystemMessage ((addRes, addView), addEncoding) <- runFormPost . identForm FIDSystemMessageAdd . renderAForm FormStandard $ SystemMessage
<$> aopt utcTimeField (fslI MsgSystemMessageFrom) Nothing <$> aopt utcTimeField (fslI MsgSystemMessageFrom) Nothing
@ -245,5 +249,5 @@ postMessageListR = do
addMessageI Success $ MsgSystemMessageAdded cID addMessageI Success $ MsgSystemMessageAdded cID
redirect $ MessageR cID redirect $ MessageR cID
defaultLayout $ do defaultLayout
$(widgetFile "system-message-list") $(widgetFile "system-message-list")

View File

@ -13,7 +13,7 @@ import qualified Database.Esqueleto as E
validateTerm :: Term -> [Text] validateTerm :: Term -> [Text]
validateTerm (Term{..}) = validateTerm Term{..} =
[ msg | (False, msg) <- [ msg | (False, msg) <-
[ --startOk [ --startOk
( termStart `withinTerm` termName ( termStart `withinTerm` termName
@ -60,15 +60,15 @@ getTermShowR = do
provideRep $ toJSON . map fst <$> runDB (E.select $ E.from termData) provideRep $ toJSON . map fst <$> runDB (E.select $ E.from termData)
provideRep $ do provideRep $ do
let colonnadeTerms = widgetColonnade $ mconcat let colonnadeTerms = widgetColonnade $ mconcat
[ sortable Nothing "Kürzel" $ [ sortable Nothing "Kürzel" $ \(Entity tid _, _) -> anchorCell
anchorCell' (\(Entity tid _, _) -> TermCourseListR tid) (TermCourseListR tid)
(\(Entity tid _, _) -> [whamlet|#{toPathPiece tid}|]) [whamlet|#{toPathPiece tid}|]
, sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) -> , sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) ->
cell $ formatTime SelFormatDate termLectureStart >>= toWidget cell $ formatTime SelFormatDate termLectureStart >>= toWidget
, sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) -> , sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
cell $ formatTime SelFormatDate termLectureEnd >>= toWidget cell $ formatTime SelFormatDate termLectureEnd >>= toWidget
, sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) -> , sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) ->
textCell $ (bool "" tickmark termActive :: Text) textCell (bool "" tickmark termActive :: Text)
, sortable Nothing "Kurse" $ \(_, E.Value numCourses) -> , sortable Nothing "Kurse" $ \(_, E.Value numCourses) ->
cell [whamlet|_{MsgNumCourses numCourses}|] cell [whamlet|_{MsgNumCourses numCourses}|]
, sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) -> , sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) ->
@ -96,7 +96,7 @@ getTermShowR = do
-- #{termToText termName} -- #{termToText termName}
-- |] -- |]
-- ] -- ]
((), table) <- dbTable def $ DBTable ((), table) <- dbTable def DBTable
{ dbtSQLQuery = termData { dbtSQLQuery = termData
, dbtColonnade = colonnadeTerms , dbtColonnade = colonnadeTerms
, dbtProj = return . dbrOutput , dbtProj = return . dbrOutput
@ -116,12 +116,12 @@ getTermShowR = do
] ]
, dbtFilter = Map.fromList , dbtFilter = Map.fromList
[ ( "active" [ ( "active"
, FilterColumn $ \term -> (term E.^. TermActive :: E.SqlExpr (E.Value Bool)) , FilterColumn $ \term -> term E.^. TermActive :: E.SqlExpr (E.Value Bool)
) )
, ( "course" , ( "course"
, FilterColumn $ \term csh -> case csh of -- FilterColumn-Lambdas are , FilterColumn $ \term csh -> case csh of -- FilterColumn-Lambdas are
[] -> E.val True :: E.SqlExpr (E.Value Bool) [] -> E.val True :: E.SqlExpr (E.Value Bool)
cshs -> E.exists . E.from $ \course -> do cshs -> E.exists . E.from $ \course ->
E.where_ $ course E.^. CourseTerm E.==. term E.^. TermId E.where_ $ course E.^. CourseTerm E.==. term E.^. TermId
E.&&. course E.^. CourseShorthand `E.in_` E.valList cshs E.&&. course E.^. CourseShorthand `E.in_` E.valList cshs
) )
@ -134,7 +134,7 @@ getTermShowR = do
$(widgetFile "terms") $(widgetFile "terms")
getTermEditR :: Handler Html getTermEditR :: Handler Html
getTermEditR = do getTermEditR =
-- TODO: Defaults für Semester hier ermitteln und übergeben -- TODO: Defaults für Semester hier ermitteln und übergeben
termEditHandler Nothing termEditHandler Nothing
@ -162,7 +162,7 @@ termEditHandler term = do
-- MIT INTERNATIONALISIERUNG: -- MIT INTERNATIONALISIERUNG:
addMessageI Success $ MsgTermEdited tid addMessageI Success $ MsgTermEdited tid
redirect TermShowR redirect TermShowR
(FormMissing ) -> return () FormMissing -> return ()
(FormFailure _) -> addMessageI Warning MsgInvalidInput (FormFailure _) -> addMessageI Warning MsgInvalidInput
let actionUrl = TermEditR let actionUrl = TermEditR
defaultLayout $ do defaultLayout $ do

View File

@ -69,7 +69,7 @@ getUsersR = do
psValidator = def psValidator = def
& defaultSorting [("name", SortAsc),("display-name", SortAsc)] & defaultSorting [("name", SortAsc),("display-name", SortAsc)]
((), userList) <- dbTable psValidator $ DBTable ((), userList) <- dbTable psValidator DBTable
{ dbtSQLQuery = return :: E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (Entity User)) { dbtSQLQuery = return :: E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (Entity User))
, dbtColonnade , dbtColonnade
, dbtProj = return , dbtProj = return
@ -106,7 +106,7 @@ postAdminHijackUserR cID = do
otherSchoolsAdmin <- Set.fromList . map (userAdminSchool . entityVal) <$> selectList [UserAdminUser ==. uid] [] otherSchoolsAdmin <- Set.fromList . map (userAdminSchool . entityVal) <$> selectList [UserAdminUser ==. uid] []
otherSchoolsLecturer <- Set.fromList . map (userLecturerSchool . entityVal) <$> selectList [UserLecturerUser ==. uid] [] otherSchoolsLecturer <- Set.fromList . map (userLecturerSchool . entityVal) <$> selectList [UserLecturerUser ==. uid] []
mySchools <- Set.fromList . map (userAdminSchool . entityVal) <$> selectList [UserAdminUser ==. myUid] [] mySchools <- Set.fromList . map (userAdminSchool . entityVal) <$> selectList [UserAdminUser ==. myUid] []
when (not $ (otherSchoolsAdmin `Set.union` otherSchoolsLecturer) `Set.isSubsetOf` mySchools) $ unless ((otherSchoolsAdmin `Set.union` otherSchoolsLecturer) `Set.isSubsetOf` mySchools) $
permissionDenied "Cannot escalate admin status to additional schools" permissionDenied "Cannot escalate admin status to additional schools"
get404 uid get404 uid

View File

@ -31,7 +31,7 @@ downloadFiles = do
return userDefaultDownloadFiles return userDefaultDownloadFiles
tidFromText :: Text -> Maybe TermId tidFromText :: Text -> Maybe TermId
tidFromText = (fmap TermKey) . maybeRight . termFromText tidFromText = fmap TermKey . maybeRight . termFromText
simpleLink :: Widget -> Route UniWorX -> Widget simpleLink :: Widget -> Route UniWorX -> Widget
simpleLink lbl url = [whamlet|<a href=@{url}>^{lbl}|] simpleLink lbl url = [whamlet|<a href=@{url}>^{lbl}|]

View File

@ -10,7 +10,7 @@ module Handler.Utils.DateTime
import Import import Import
import Data.Time.Zones hiding (localTimeToUTCFull) import Data.Time.Zones
import qualified Data.Time.Zones as TZ import qualified Data.Time.Zones as TZ
import Data.Time hiding (formatTime, localTimeToUTC, utcToLocalTime) import Data.Time hiding (formatTime, localTimeToUTC, utcToLocalTime)
@ -20,8 +20,6 @@ import qualified Data.Time.Format as Time
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
import Mail
utcToLocalTime :: UTCTime -> LocalTime utcToLocalTime :: UTCTime -> LocalTime
utcToLocalTime = TZ.utcToLocalTimeTZ appTZ utcToLocalTime = TZ.utcToLocalTimeTZ appTZ
@ -38,7 +36,7 @@ instance HasLocalTime Day where
toLocalTime d = LocalTime d midnight toLocalTime d = LocalTime d midnight
instance HasLocalTime UTCTime where instance HasLocalTime UTCTime where
toLocalTime t = utcToLocalTime t toLocalTime = utcToLocalTime
formatTime' :: (HasLocalTime t, MonadHandler m, HandlerSite m ~ UniWorX) => String -> t -> m Text formatTime' :: (HasLocalTime t, MonadHandler m, HandlerSite m ~ UniWorX) => String -> t -> m Text
formatTime' fmtStr t = fmap fromString $ Time.formatTime <$> getTimeLocale <*> pure fmtStr <*> pure (toLocalTime t) formatTime' fmtStr t = fmap fromString $ Time.formatTime <$> getTimeLocale <*> pure fmtStr <*> pure (toLocalTime t)
@ -80,7 +78,7 @@ getDateTimeFormat sel = do
validDateTimeFormats :: TimeLocale -> SelDateTimeFormat -> Set DateTimeFormat validDateTimeFormats :: TimeLocale -> SelDateTimeFormat -> Set DateTimeFormat
-- ^ We use a whitelist instead of just letting the user specify their own format string since vulnerabilities in printf-like functions are not uncommon -- ^ We use a whitelist instead of just letting the user specify their own format string since vulnerabilities in printf-like functions are not uncommon
validDateTimeFormats _ SelFormatDateTime = Set.fromList $ validDateTimeFormats _ SelFormatDateTime = Set.fromList
[ DateTimeFormat "%a %d %b %Y %R" [ DateTimeFormat "%a %d %b %Y %R"
, DateTimeFormat "%a %b %d %Y %R" , DateTimeFormat "%a %b %d %Y %R"
, DateTimeFormat "%A, %d %B %Y %R" , DateTimeFormat "%A, %d %B %Y %R"
@ -97,7 +95,7 @@ validDateTimeFormats _ SelFormatDateTime = Set.fromList $
, DateTimeFormat "%Y-%m-%d %T" , DateTimeFormat "%Y-%m-%d %T"
, DateTimeFormat "%Y-%m-%dT%T" , DateTimeFormat "%Y-%m-%dT%T"
] ]
validDateTimeFormats _ SelFormatDate = Set.fromList $ validDateTimeFormats _ SelFormatDate = Set.fromList
[ DateTimeFormat "%a %d %b %Y" [ DateTimeFormat "%a %d %b %Y"
, DateTimeFormat "%a %b %d %Y" , DateTimeFormat "%a %b %d %Y"
, DateTimeFormat "%A, %d %B %Y" , DateTimeFormat "%A, %d %B %Y"
@ -128,7 +126,7 @@ dateTimeFormatOptions sel = do
let let
toOption fmt@DateTimeFormat{..} = do toOption fmt@DateTimeFormat{..} = do
dateTime <- formatTime' unDateTimeFormat now dateTime <- formatTime' unDateTimeFormat now
return $ (dateTime, fmt) return (dateTime, fmt)
optionsPairs <=< mapM toOption . Set.toList $ validDateTimeFormats tl sel optionsPairs <=< mapM toOption . Set.toList $ validDateTimeFormats tl sel

View File

@ -6,16 +6,12 @@ module Handler.Utils.Form
import Utils.Form import Utils.Form
import Handler.Utils.Form.Types import Handler.Utils.Form.Types
import Handler.Utils.Templates
import Handler.Utils.DateTime import Handler.Utils.DateTime
import qualified Data.Time as Time
import Import hiding (cons) import Import hiding (cons)
import qualified Data.Char as Char import qualified Data.Char as Char
import Data.String (IsString(..))
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Foldable as Foldable import qualified Data.Foldable as Foldable
@ -32,10 +28,8 @@ 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 as E
import qualified Database.Esqueleto.Internal.Sql as E
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set
import Data.Map (Map) import Data.Map (Map)
import qualified Data.Map as Map import qualified Data.Map as Map
@ -137,7 +131,7 @@ buttonForm csrf = do
buttonIdent <- newFormIdent buttonIdent <- newFormIdent
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
(results, btnViews) <- unzip <$> mapM button [minBound..maxBound] (results, btnViews) <- unzip <$> mapM button [minBound..maxBound]
let widget = do let widget =
[whamlet| [whamlet|
#{csrf} #{csrf}
$forall bView <- btnViews $forall bView <- btnViews
@ -169,16 +163,16 @@ natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMess
natFieldI msg = checkBool (>= 0) msg intField natFieldI msg = checkBool (>= 0) msg intField
natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
natField d = checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") $ intField natField d = checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") intField
natIntField ::(Monad m, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m Integer natIntField ::(Monad m, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m Integer
natIntField = natField natIntField = natField
posIntField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i posIntField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
posIntField d = checkBool (>= 1) (T.append d " muss eine positive Zahl sein.") $ intField posIntField d = checkBool (>= 1) (T.append d " muss eine positive Zahl sein.") intField
minIntField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => i -> Text -> Field m i minIntField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => i -> Text -> Field m i
minIntField m d = checkBool (>= m) (T.concat [d," muss größer als ", T.pack $ show m, " sein."]) $ intField minIntField m d = checkBool (>= m) (T.concat [d," muss größer als ", T.pack $ show m, " sein."]) intField
pointsField :: (Monad m, HandlerSite m ~ UniWorX) => Field m Points --TODO allow fractions pointsField :: (Monad m, HandlerSite m ~ UniWorX) => Field m Points --TODO allow fractions
pointsField = checkBool (>= 0) MsgPointsNotPositive Field{..} pointsField = checkBool (>= 0) MsgPointsNotPositive Field{..}
@ -261,7 +255,7 @@ zipFileField doUnpack = Field{..}
| [f] <- files = return . Right . Just $ bool (yieldM . acceptFile) sourceFiles doUnpack f | [f] <- files = return . Right . Just $ bool (yieldM . acceptFile) sourceFiles doUnpack f
| null files = return $ Right Nothing | null files = return $ Right Nothing
| otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile | otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile
fieldView fieldId fieldName attrs _ req = do fieldView fieldId fieldName attrs _ req =
[whamlet| [whamlet|
$newline never $newline never
<input type=file ##{fieldId} *{attrs} name=#{fieldName} :req:required> <input type=file ##{fieldId} *{attrs} name=#{fieldName} :req:required>
@ -290,7 +284,7 @@ multiFileField permittedFiles' = Field{..}
mapM_ handleFile files .| C.map Right mapM_ handleFile files .| C.map Right
where where
doUnpack = unpackZips `elem` vals doUnpack = unpackZips `elem` vals
fieldView fieldId fieldName attrs val req = do fieldView fieldId fieldName _attrs val req = do
pVals <- handlerToWidget permittedFiles' pVals <- handlerToWidget permittedFiles'
sentVals <- for val $ \src -> handlerToWidget . sourceToList $ src .| takeLefts sentVals <- for val $ \src -> handlerToWidget . sourceToList $ src .| takeLefts
let let
@ -460,7 +454,7 @@ utcTimeField :: (MonadHandler m, HandlerSite m ~ UniWorX) => Field m UTCTime
-- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing) -- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing)
-- Browser returns LocalTime -- Browser returns LocalTime
utcTimeField = Field utcTimeField = Field
{ fieldParse = parseHelperGen $ readTime { fieldParse = parseHelperGen readTime
, fieldView = \theId name attrs val isReq -> do , fieldView = \theId name attrs val isReq -> do
val' <- either id id <$> traverse (formatTime' fieldTimeFormat) val val' <- either id id <$> traverse (formatTime' fieldTimeFormat) val
[whamlet| [whamlet|
@ -478,10 +472,10 @@ utcTimeField = Field
readTime :: Text -> Either UniWorXMessage UTCTime readTime :: Text -> Either UniWorXMessage UTCTime
readTime t = readTime t =
case localTimeToUTC <$> parseTimeM True defaultTimeLocale fieldTimeFormat (T.unpack t) of case localTimeToUTC <$> parseTimeM True defaultTimeLocale fieldTimeFormat (T.unpack t) of
(Just (LTUUnique time _)) -> Right time Just LTUUnique{_ltuResult} -> Right _ltuResult
(Just (LTUNone _ _)) -> Left MsgIllDefinedUTCTime Just LTUNone{} -> Left MsgIllDefinedUTCTime
(Just (LTUAmbiguous _ _ _ _)) -> Left MsgAmbiguousUTCTime Just LTUAmbiguous{} -> Left MsgAmbiguousUTCTime
Nothing -> Left MsgInvalidDateTimeFormat Nothing -> Left MsgInvalidDateTimeFormat
langField :: Bool -- ^ Only allow values from `appLanguages` langField :: Bool -- ^ Only allow values from `appLanguages`
-> Field (HandlerT UniWorX IO) Lang -> Field (HandlerT UniWorX IO) Lang
@ -511,7 +505,7 @@ optionsPersistCryptoId filts ords toDisplay = fmap mkOptionList $ do
mr <- getMessageRender mr <- getMessageRender
pairs <- runDB $ selectList filts ords pairs <- runDB $ selectList filts ords
cPairs <- forM pairs $ \e@(Entity key _) -> (,) <$> encrypt key <*> pure e cPairs <- forM pairs $ \e@(Entity key _) -> (,) <$> encrypt key <*> pure e
return $ map (\(cId, e@(Entity key value)) -> Option return $ map (\(cId, e@(Entity _key value)) -> Option
{ optionDisplay = mr (toDisplay value) { optionDisplay = mr (toDisplay value)
, optionInternalValue = e , optionInternalValue = e
, optionExternalValue = toPathPiece (cId :: CryptoID UUID (Key a)) , optionExternalValue = toPathPiece (cId :: CryptoID UUID (Key a))
@ -557,13 +551,13 @@ apreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
-- ^ Pseudo required -- ^ Pseudo required
apreq f fs mx = formToAForm $ do apreq f fs mx = formToAForm $ do
mr <- getMessageRender mr <- getMessageRender
fmap (over _1 (maybe (FormFailure [mr MsgValueRequired]) return =<<) . over _2 (pure . (\fv -> fv { fvRequired = True } ))) $ mopt f fs (Just <$> mx) over _1 (maybe (FormFailure [mr MsgValueRequired]) return =<<) . over _2 (pure . (\fv -> fv { fvRequired = True } )) <$> mopt f fs (Just <$> mx)
wpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) wpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a) => Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a)
wpreq f fs mx = mFormToWForm $ do wpreq f fs mx = mFormToWForm $ do
mr <- getMessageRender mr <- getMessageRender
fmap (over _1 (maybe (FormFailure [mr MsgValueRequired]) return =<<) . over _2 (\fv -> fv { fvRequired = True } )) $ mopt f fs (Just <$> mx) over _1 (maybe (FormFailure [mr MsgValueRequired]) return =<<) . over _2 (\fv -> fv { fvRequired = True } ) <$> mopt f fs (Just <$> mx)
multiAction :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action) multiAction :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action)
=> Map action (AForm (HandlerT UniWorX IO) a) => Map action (AForm (HandlerT UniWorX IO) a)
@ -579,7 +573,7 @@ multiAction acts defAction = do
mToWidget aForm = Just . snd <$> renderAForm FormStandard (formToAForm $ return aForm) mempty mToWidget aForm = Just . snd <$> renderAForm FormStandard (formToAForm $ return aForm) mempty
widgets <- mapM mToWidget results widgets <- mapM mToWidget results
let actionWidgets = Map.foldrWithKey accWidget [] widgets let actionWidgets = Map.foldrWithKey accWidget [] widgets
accWidget act Nothing = id accWidget _act Nothing = id
accWidget act (Just w) = cons $(widgetFile "widgets/multiAction") accWidget act (Just w) = cons $(widgetFile "widgets/multiAction")
actionResults = Map.map fst results actionResults = Map.map fst results
return ((actionResults Map.!) =<< actionRes, $(widgetFile "widgets/multiActionCollect")) return ((actionResults Map.!) =<< actionRes, $(widgetFile "widgets/multiActionCollect"))

View File

@ -4,7 +4,7 @@ module Handler.Utils.Mail
, addFileDB , addFileDB
) where ) where
import Import hiding ((.=)) import Import
import Utils.Lens hiding (snoc) import Utils.Lens hiding (snoc)

View File

@ -13,7 +13,7 @@ module Handler.Utils.Rating
, extractRatings , extractRatings
) where ) where
import Import hiding ((</>)) import Import
import Text.PrettyPrint.Leijen.Text hiding ((<$>)) import Text.PrettyPrint.Leijen.Text hiding ((<$>))
@ -56,9 +56,9 @@ instance Pretty x => Pretty (CI x) where
instance Pretty SheetGrading where instance Pretty SheetGrading where
pretty (Points {..}) = pretty ( (show maxPoints) <> " Punkte" :: String) pretty Points{..} = pretty ( show maxPoints <> " Punkte" :: String)
pretty (PassPoints {..}) = pretty ( (show maxPoints) <> " Punkte, bestanden ab " <> (show passingPoints) <> " Punkte" :: String ) pretty PassPoints{..} = pretty ( show maxPoints <> " Punkte, bestanden ab " <> show passingPoints <> " Punkte" :: String )
pretty (PassBinary) = pretty ( "Bestanden (1) / Nicht bestanden (0)" :: String ) pretty PassBinary = pretty ( "Bestanden (1) / Nicht bestanden (0)" :: String )
data Rating = Rating data Rating = Rating
@ -138,10 +138,10 @@ formatRating cID Rating{ ratingValues = Rating'{..}, ..} = let
in Lazy.Text.encodeUtf8 . (<> "\n") $ displayT doc in Lazy.Text.encodeUtf8 . (<> "\n") $ displayT doc
ratingFile :: MonadIO m => CryptoFileNameSubmission -> Rating -> m File ratingFile :: MonadIO m => CryptoFileNameSubmission -> Rating -> m File
ratingFile cID rating@(Rating{ ratingValues = Rating'{..}, .. }) = do ratingFile cID rating@Rating{ ratingValues = Rating'{..}, .. } = do
fileModified <- maybe (liftIO getCurrentTime) return ratingTime fileModified <- maybe (liftIO getCurrentTime) return ratingTime
let let
fileTitle = "bewertung_" <> (Text.unpack $ toPathPiece (cID :: CryptoFileNameSubmission)) <.> "txt" fileTitle = "bewertung_" <> Text.unpack (toPathPiece cID) <.> "txt"
fileContent = Just . Lazy.ByteString.toStrict $ formatRating cID rating fileContent = Just . Lazy.ByteString.toStrict $ formatRating cID rating
return File{..} return File{..}
@ -149,10 +149,10 @@ parseRating :: MonadThrow m => File -> m Rating'
parseRating File{ fileContent = Just input, .. } = do parseRating File{ fileContent = Just input, .. } = do
inputText <- either (throw . RatingNotUnicode) return $ Text.decodeUtf8' input inputText <- either (throw . RatingNotUnicode) return $ Text.decodeUtf8' input
let let
(headerLines', commentLines) = break (sep `Text.isInfixOf`) $ Text.lines inputText (headerLines', commentLines) = break (commentSep `Text.isInfixOf`) $ Text.lines inputText
(reverse -> ratingLines, reverse -> headerLines) = break (sep' `Text.isInfixOf`) $ reverse headerLines' (reverse -> ratingLines, reverse -> _headerLines) = break (sep' `Text.isInfixOf`) $ reverse headerLines'
ratingLines' = filter (rating `Text.isInfixOf`) ratingLines ratingLines' = filter (rating `Text.isInfixOf`) ratingLines
sep = "Beginn der Kommentare" commentSep = "Beginn der Kommentare"
sep' = Text.pack $ replicate 40 '=' sep' = Text.pack $ replicate 40 '='
rating = "Bewertung:" rating = "Bewertung:"
comment' <- case commentLines of comment' <- case commentLines of
@ -162,7 +162,7 @@ parseRating File{ fileContent = Just input, .. } = do
ratingComment ratingComment
| Text.null comment' = Nothing | Text.null comment' = Nothing
| otherwise = Just comment' | otherwise = Just comment'
ratingLine' <- case ratingLines of ratingLine' <- case ratingLines' of
[l] -> return l [l] -> return l
_ -> throw RatingMultiple _ -> throw RatingMultiple
let let

View File

@ -37,8 +37,8 @@ pKey :: Parser Int
pKey = decimal pKey = decimal
pType :: Parser StudyFieldType pType :: Parser StudyFieldType
pType = FieldPrimary <$ (try $ string "HF") pType = FieldPrimary <$ try (string "HF")
<|> FieldSecondary <$ (try $ string "NF") <|> FieldSecondary <$ try (string "NF")
decimal :: Parser Int decimal :: Parser Int
decimal = foldl' (\now next -> now * 10 + next) 0 <$> many1 digit' decimal = foldl' (\now next -> now * 10 + next) 0 <$> many1 digit'

View File

@ -9,13 +9,11 @@ module Handler.Utils.Submission
, submissionMatchesSheet , submissionMatchesSheet
) where ) where
import Import hiding ((.=), joinPath) import Import hiding (joinPath)
import Jobs import Jobs
import Prelude (lcm) import Prelude (lcm)
import Yesod.Core.Types (HandlerContents(..), ErrorResponse(..)) import Yesod.Core.Types (HandlerContents(..), ErrorResponse(..))
import Control.Lens
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)
@ -29,15 +27,12 @@ import Data.Maybe ()
import qualified Data.List as List import qualified Data.List as List
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
import Data.Map (Map, (!?)) import Data.Map (Map)
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Text as Text import qualified Data.Text as Text
import Data.Ratio import Data.Ratio
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Data.Monoid (Monoid, Any(..), Sum(..)) import Data.Monoid (Monoid, Any(..), Sum(..))
import Generics.Deriving.Monoid (memptydefault, mappenddefault) import Generics.Deriving.Monoid (memptydefault, mappenddefault)
@ -49,7 +44,6 @@ 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
@ -76,7 +70,7 @@ assignSubmissions sid restriction = do
Sheet{..} <- getJust sid Sheet{..} <- getJust sid
correctors <- selectList [ SheetCorrectorSheet ==. sid, SheetCorrectorState ==. CorrectorNormal ] [] correctors <- selectList [ SheetCorrectorSheet ==. sid, SheetCorrectorState ==. CorrectorNormal ] []
let let
byTutorial' uid = join . Map.lookup uid $ Map.fromList [ (sheetCorrectorUser, byTutorial sheetCorrectorLoad) | Entity _ SheetCorrector{..} <- corrsTutorial ] -- byTutorial' uid = join . Map.lookup uid $ Map.fromList [ (sheetCorrectorUser, byTutorial sheetCorrectorLoad) | Entity _ SheetCorrector{..} <- corrsTutorial ]
corrsTutorial = filter hasTutorialLoad correctors -- needed as List within Esqueleto corrsTutorial = filter hasTutorialLoad correctors -- needed as List within Esqueleto
corrsProp = filter hasPositiveLoad correctors corrsProp = filter hasPositiveLoad correctors
countsToLoad' :: UserId -> Bool countsToLoad' :: UserId -> Bool
@ -118,7 +112,7 @@ assignSubmissions sid restriction = do
let let
prevSubs' :: Map SheetId (Map UserId (Rational, Integer)) prevSubs' :: Map SheetId (Map UserId (Rational, Integer))
prevSubs' = Map.unionsWith (Map.unionWith $ \(prop, n) (_, n') -> (prop, n + n')) $ do prevSubs' = Map.unionsWith (Map.unionWith $ \(prop, n) (_, n') -> (prop, n + n')) $ do
(Entity _ sc@SheetCorrector{ sheetCorrectorLoad = Load{..}, .. }, E.Value isByTutorial, E.Value isPlaceholder) <- prevSubs (Entity _ SheetCorrector{ sheetCorrectorLoad = Load{..}, .. }, E.Value isByTutorial, E.Value isPlaceholder) <- prevSubs
guard $ maybe True (not isByTutorial ||) byTutorial guard $ maybe True (not isByTutorial ||) byTutorial
let proportion let proportion
| CorrectorExcused <- sheetCorrectorState = 0 | CorrectorExcused <- sheetCorrectorState = 0
@ -311,9 +305,9 @@ extractRatingsMsg :: ( MonadHandler m
) => Conduit File m SubmissionContent ) => Conduit File m SubmissionContent
extractRatingsMsg = do extractRatingsMsg = do
ignored' <- filterSubmission `fuseUpstream` Rating.extractRatings ignored' <- filterSubmission `fuseUpstream` Rating.extractRatings
let ignored :: Set (Either CryptoFileNameSubmission FilePath) let ignoredFiles :: Set (Either CryptoFileNameSubmission FilePath)
ignored = Right `Set.map` ignored' ignoredFiles = Right `Set.map` ignored'
unless (null ignored) $ do unless (null ignoredFiles) $ do
mr <- (toHtml . ) <$> getMessageRender mr <- (toHtml . ) <$> getMessageRender
addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionFilesIgnored.hamlet") mr) addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionFilesIgnored.hamlet") mr)
@ -346,20 +340,19 @@ sinkSubmission userId mExists isUpdate = do
return sId return sId
Right sId -> return sId Right sId -> return sId
sId <$ sinkSubmission' sId isUpdate sId <$ sinkSubmission' sId
where where
tell = modify . mappend tellSt = modify . mappend
sinkSubmission' :: SubmissionId sinkSubmission' :: SubmissionId
-> Bool -- ^ Is this a correction
-> Sink SubmissionContent (YesodJobDB UniWorX) () -> Sink SubmissionContent (YesodJobDB UniWorX) ()
sinkSubmission' submissionId isUpdate = lift . finalize <=< execStateLC mempty . Conduit.mapM_ $ \case sinkSubmission' submissionId = 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 } tellSt $ mempty{ sinkFilenames = Set.singleton fileTitle }
otherVersions <- lift . E.select . E.from $ \(sf `E.InnerJoin` f) -> do otherVersions <- lift . E.select . E.from $ \(sf `E.InnerJoin` f) -> do
E.on $ sf E.^. SubmissionFileFile E.==. f E.^. FileId E.on $ sf E.^. SubmissionFileFile E.==. f E.^. FileId
@ -411,7 +404,7 @@ sinkSubmission userId mExists isUpdate = do
alreadySeen <- gets $ getAny . sinkSeenRating alreadySeen <- gets $ getAny . sinkSeenRating
when alreadySeen $ throwM DuplicateRating when alreadySeen $ throwM DuplicateRating
tell $ mempty{ sinkSeenRating = Any True } tellSt $ mempty{ sinkSeenRating = Any True }
unless isUpdate $ throwM RatingWithoutUpdate unless isUpdate $ throwM RatingWithoutUpdate
@ -459,10 +452,10 @@ sinkSubmission userId mExists isUpdate = do
False -> lift . insert_ $ SubmissionEdit userId now submissionId False -> lift . insert_ $ SubmissionEdit userId now submissionId
True -> do True -> do
Submission{submissionRatingTime} <- lift $ getJust submissionId Submission{submissionRatingTime} <- lift $ getJust submissionId
when (isNothing submissionRatingTime) $ tell mempty { sinkSubmissionNotifyRating = Any True } when (isNothing submissionRatingTime) $ tellSt mempty { sinkSubmissionNotifyRating = Any True }
lift $ update submissionId [ SubmissionRatingBy =. Just userId, SubmissionRatingTime =. Just now ] lift $ update submissionId [ SubmissionRatingBy =. Just userId, SubmissionRatingTime =. Just now ]
-- TODO: Should submissionRatingAssigned change here if userId changes? -- TODO: Should submissionRatingAssigned change here if userId changes?
tell $ mempty{ sinkSubmissionTouched = Any True } tellSt $ mempty{ sinkSubmissionTouched = Any True }
finalize :: SubmissionSinkState -> YesodJobDB UniWorX () finalize :: SubmissionSinkState -> YesodJobDB UniWorX ()
finalize SubmissionSinkState{..} = do finalize SubmissionSinkState{..} = do
@ -515,9 +508,9 @@ sinkSubmission userId mExists isUpdate = do
data SubmissionMultiSinkException data SubmissionMultiSinkException
= SubmissionSinkException = SubmissionSinkException
{ submissionSinkId :: CryptoFileNameSubmission { _submissionSinkId :: CryptoFileNameSubmission
, submissionSinkFedFile :: Maybe FilePath , _submissionSinkFedFile :: Maybe FilePath
, submissionSinkException :: SubmissionSinkException , _submissionSinkException :: SubmissionSinkException
} }
deriving (Typeable, Show) deriving (Typeable, Show)
@ -559,7 +552,7 @@ 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, ignored) <- execRWSLC () Map.empty . awaitForever $ \case (sinks, ignoredFiles) <- execRWSLC () Map.empty . awaitForever $ \case
v@(Right (sId, _)) -> do v@(Right (sId, _)) -> do
cID <- encrypt sId cID <- encrypt sId
$logDebugS "sinkMultiSubmission" $ "Feeding rating for " <> toPathPiece cID $logDebugS "sinkMultiSubmission" $ "Feeding rating for " <> toPathPiece cID
@ -586,7 +579,7 @@ sinkMultiSubmission userId isUpdate = do
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 when (not $ null ignoredFiles) $ do
mr <- (toHtml .) <$> getMessageRender mr <- (toHtml .) <$> getMessageRender
addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionFilesIgnored.hamlet") mr) addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionFilesIgnored.hamlet") mr)
lift . fmap Set.fromList . forM (Map.toList sinks) $ \(sId, sink) -> do lift . fmap Set.fromList . forM (Map.toList sinks) $ \(sId, sink) -> do

View File

@ -1,8 +1,7 @@
module Handler.Utils.Table where module Handler.Utils.Table where
-- General Utilities for Tables -- General Utilities for Tables
import Import hiding ((<>)) import Import
-- import Data.Monoid ((<>))
import Data.Profunctor import Data.Profunctor
import Control.Monad.Except import Control.Monad.Except
@ -59,11 +58,11 @@ headedRowSelector toExternal fromExternal attrs colonnade tdata = do
extIds <- maybe (throwError "Error parsing values") return $ mapM fromPathPiece optlist extIds <- maybe (throwError "Error parsing values") return $ mapM fromPathPiece optlist
case () of case () of
_ | extId `elem` extIds _ | extId `elem` extIds
-> Just <$> (lift $ fromExternal extId) -> Just <$> lift (fromExternal extId)
| otherwise | otherwise
-> return Nothing -> return Nothing
view _ name attributes val _ = do view _ name attributes val _ =
[whamlet| [whamlet|
<label style="display: block"> <label style="display: block">
<input type=checkbox name=#{name} value=#{toPathPiece extId} *{attributes} :isRight val:checked> <input type=checkbox name=#{name} value=#{toPathPiece extId} *{attributes} :isRight val:checked>

View File

@ -63,7 +63,7 @@ courseCellCL (tid,ssh,csh) = anchorCell link name
name = citext2widget csh name = citext2widget csh
courseCell :: IsDBTable m a => Course -> DBCell m a courseCell :: IsDBTable m a => Course -> DBCell m a
courseCell (Course {..}) = anchorCell link name `mappend` desc courseCell Course{..} = anchorCell link name `mappend` desc
where where
link = CourseR courseTerm courseSchool courseShorthand CShowR link = CourseR courseTerm courseSchool courseShorthand CShowR
name = citext2widget courseName name = citext2widget courseName
@ -84,7 +84,7 @@ sheetCell crse shn =
in anchorCell link $ display2widget shn in anchorCell link $ display2widget shn
sheetTypeCell :: IsDBTable m a => SheetType -> DBCell m a sheetTypeCell :: IsDBTable m a => SheetType -> DBCell m a
sheetTypeCell st = i18nCell $ SheetTypeComplete st sheetTypeCell sheetType = i18nCell $ SheetTypeComplete sheetType
submissionCell :: IsDBTable m a => CourseLink -> SheetName -> SubmissionId -> DBCell m a submissionCell :: IsDBTable m a => CourseLink -> SheetName -> SubmissionId -> DBCell m a
submissionCell crse shn sid = submissionCell crse shn sid =
@ -93,7 +93,7 @@ submissionCell crse shn sid =
csh = crse ^. _3 csh = crse ^. _3
mkCid = encrypt sid mkCid = encrypt sid
mkRoute cid = CSubmissionR tid ssh csh shn cid SubShowR mkRoute cid = CSubmissionR tid ssh csh shn cid SubShowR
mkText cid = display2widget cid mkText = display2widget
in anchorCellM' mkCid mkRoute mkText in anchorCellM' mkCid mkRoute mkText
correctorStateCell :: IsDBTable m a => SheetCorrector -> DBCell m a correctorStateCell :: IsDBTable m a => SheetCorrector -> DBCell m a

View File

@ -27,13 +27,10 @@ module Handler.Utils.Table.Pagination
import Handler.Utils.Table.Pagination.Types import Handler.Utils.Table.Pagination.Types
import Utils.Lens.TH import Utils.Lens.TH
import Import import Import hiding (pi)
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect,unsafeSqlValue) import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect,unsafeSqlValue)
import qualified Database.Esqueleto.Internal.Language as E (From) 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 qualified Data.Binary.Builder as Builder import qualified Data.Binary.Builder as Builder
@ -42,8 +39,8 @@ import qualified Network.Wai as Wai
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Control.Monad.RWS hiding ((<>), Foldable(..), mapM_, forM_) import Control.Monad.RWS hiding ((<>), mapM_)
import Control.Monad.Writer hiding ((<>), Foldable(..), mapM_, forM_) import Control.Monad.Writer hiding ((<>), mapM_)
import Control.Monad.Reader (ReaderT(..), mapReaderT) import Control.Monad.Reader (ReaderT(..), mapReaderT)
import Control.Monad.Trans.Maybe import Control.Monad.Trans.Maybe
@ -52,8 +49,6 @@ import Data.Foldable (Foldable(foldMap))
import Data.Map (Map, (!)) import Data.Map (Map, (!))
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Profunctor (lmap)
import Colonnade hiding (bool, fromMaybe, singleton) import Colonnade hiding (bool, fromMaybe, singleton)
import qualified Colonnade (singleton) import qualified Colonnade (singleton)
import Colonnade.Encode import Colonnade.Encode
@ -64,8 +59,6 @@ import Data.Ratio ((%))
import Control.Lens import Control.Lens
import Data.Proxy
data SortColumn t = forall a. PersistField a => SortColumn { getSortColumn :: t -> E.SqlExpr (E.Value a) } data SortColumn t = forall a. PersistField a => SortColumn { getSortColumn :: t -> E.SqlExpr (E.Value a) }
@ -99,13 +92,13 @@ instance IsFilterColumn t cont => IsFilterColumn t (t -> cont) where
filterColumn' cont is t = filterColumn' (cont t) is t filterColumn' cont is t = filterColumn' (cont t) is t
instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterColumn t cont, MonoPointed l, Monoid l) => IsFilterColumn t (l -> cont) where instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterColumn t cont, MonoPointed l, Monoid l) => IsFilterColumn t (l -> cont) where
filterColumn' cont is t = filterColumn' (cont input) is' t filterColumn' cont is = filterColumn' (cont input) is'
where where
(input, ($ []) -> is') = go (mempty, id) is (input, ($ []) -> is') = go (mempty, id) is
go acc [] = acc go acc [] = acc
go (acc, is') (i:is) go (acc, is3) (i:is2)
| Just i' <- fromPathPiece i = go (acc `mappend` singleton i', is') is | Just i' <- fromPathPiece i = go (acc `mappend` singleton i', is3) is2
| otherwise = go (acc, is' . (i:)) is | otherwise = go (acc, is3 . (i:)) is2
data PaginationSettings = PaginationSettings data PaginationSettings = PaginationSettings
{ psSorting :: [(CI Text, SortDirection)] { psSorting :: [(CI Text, SortDirection)]
@ -181,26 +174,26 @@ instance Default (PSValidator m x) where
asks piShortcircuit >>= (\s -> modify $ \ps -> ps { psShortcircuit = s }) asks piShortcircuit >>= (\s -> modify $ \ps -> ps { psShortcircuit = s })
defaultFilter :: Map (CI Text) [Text] -> PSValidator m x -> PSValidator m x defaultFilter :: Map (CI Text) [Text] -> PSValidator m x -> PSValidator m x
defaultFilter psFilter (runPSValidator -> f) = PSValidator $ \dbTable -> injectDefault <*> f dbTable defaultFilter psFilter (runPSValidator -> f) = PSValidator $ \dbTable' -> injectDefault <*> f dbTable'
where where
injectDefault x = case x >>= piFilter of injectDefault x = case x >>= piFilter of
Just _ -> id Just _ -> id
Nothing -> set (_2._psFilter) psFilter Nothing -> set (_2._psFilter) psFilter
defaultSorting :: [(CI Text, SortDirection)] -> PSValidator m x -> PSValidator m x defaultSorting :: [(CI Text, SortDirection)] -> PSValidator m x -> PSValidator m x
defaultSorting psSorting (runPSValidator -> f) = PSValidator $ \dbTable -> injectDefault <*> f dbTable defaultSorting psSorting (runPSValidator -> f) = PSValidator $ \dbTable' -> injectDefault <*> f dbTable'
where where
injectDefault x = case x >>= piSorting of injectDefault x = case x >>= piSorting of
Just _ -> id Just _ -> id
Nothing -> set (_2._psSorting) psSorting Nothing -> set (_2._psSorting) psSorting
restrictFilter :: (CI Text -> [Text] -> Bool) -> PSValidator m x -> PSValidator m 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 restrictFilter restrict (runPSValidator -> f) = PSValidator $ \dbTable' ps -> over _2 restrict' $ f dbTable' ps
where where
restrict' p = p { psFilter = Map.filterWithKey restrict $ psFilter p } restrict' p = p { psFilter = Map.filterWithKey restrict $ psFilter p }
restrictSorting :: (CI Text -> SortDirection -> Bool) -> PSValidator m x -> PSValidator m x restrictSorting :: (CI Text -> SortDirection -> Bool) -> PSValidator m x -> PSValidator m x
restrictSorting restrict (runPSValidator -> f) = PSValidator $ \dbTable ps -> over _2 restrict' $ f dbTable ps restrictSorting restrict (runPSValidator -> f) = PSValidator $ \dbTable' ps -> over _2 restrict' $ f dbTable' ps
where where
restrict' p = p { psSorting = filter (uncurry restrict) $ psSorting p } restrict' p = p { psSorting = filter (uncurry restrict) $ psSorting p }
@ -268,12 +261,12 @@ instance Monoid x => IsDBTable (HandlerT UniWorX IO) x where
dbCell = iso dbCell = iso
(\WidgetCell{..} -> (wgtCellAttrs, wgtCellContents)) (\WidgetCell{..} -> (wgtCellAttrs, wgtCellContents))
(\(attrs, mkWidget) -> WidgetCell attrs mkWidget) (uncurry WidgetCell)
-- dbWidget Proxy Proxy = iso (, ()) $ view _1 -- dbWidget Proxy Proxy = iso (, ()) $ view _1
dbWidget _ = return . snd dbWidget _ = return . snd
dbHandler _ f = return . over _2 f dbHandler _ f = return . over _2 f
runDBTable act = liftHandlerT act runDBTable = liftHandlerT
instance Monoid x => Monoid (DBCell (HandlerT UniWorX IO) x) where instance Monoid x => Monoid (DBCell (HandlerT UniWorX IO) x) where
mempty = WidgetCell mempty $ return mempty mempty = WidgetCell mempty $ return mempty
@ -289,7 +282,7 @@ instance Monoid x => IsDBTable (ReaderT SqlBackend (HandlerT UniWorX IO)) x wher
dbCell = iso dbCell = iso
(\DBCell{..} -> (dbCellAttrs, dbCellContents)) (\DBCell{..} -> (dbCellAttrs, dbCellContents))
(\(attrs, mkWidget) -> DBCell attrs mkWidget) (uncurry DBCell)
dbWidget _ = return . snd dbWidget _ = return . snd
dbHandler _ f = return . over _2 f dbHandler _ f = return . over _2 f
@ -319,8 +312,8 @@ instance Monoid a => IsDBTable (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enc
-- dbWidget Proxy Proxy = iso ((,) <$> view (_1._2) <*> ((,) <$> view (_1._1) <*> view _2)) -- dbWidget Proxy Proxy = iso ((,) <$> view (_1._2) <*> ((,) <$> view (_1._1) <*> view _2))
-- ((,) <$> ((,) <$> view (_2._1) <*> view _1) <*> view (_2._2)) -- ((,) <$> ((,) <$> view (_2._1) <*> view _1) <*> view (_2._2))
dbWidget DBTable{ dbtIdent = (toPathPiece -> dbtIdent) } = liftHandlerT . fmap (view $ _1 . _2) . runFormPost dbWidget _ = liftHandlerT . fmap (view $ _1 . _2) . runFormPost
dbHandler DBTable{ dbtIdent = (toPathPiece -> dbtIdent) } f form = return $ \csrf -> over _2 f <$> form csrf dbHandler _ f form = return $ fmap (over _2 f) . form
-- runDBTable :: MForm (HandlerT UniWorX IO) (FormResult a, Widget) -> m ((FormResult a, Widget), Enctype) -- runDBTable :: MForm (HandlerT UniWorX IO) (FormResult a, Widget) -> m ((FormResult a, Widget), Enctype)
-- runDBTable form = liftHandlerT . runFormPost $ \html -> over _2 (<> toWidget html) <$> form -- 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 :: MForm (HandlerT UniWorX IO) (FormResult a, Widget) -> m (Html -> MForm (HandleT UniWorX IO) (FormResult a, Widget))
@ -335,7 +328,7 @@ instance IsDBTable m a => IsString (DBCell m a) where
dbTable :: forall m x. IsDBTable m x => PSValidator m x -> DBTable m x -> Handler (DBResult m x) dbTable :: forall m x. IsDBTable m x => PSValidator m x -> DBTable m x -> Handler (DBResult m x)
dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent), dbtStyle = DBStyle{..}, .. }) = do dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = (toPathPiece -> dbtIdent), dbtStyle = DBStyle{..}, .. } = do
let let
sortingOptions = mkOptionList sortingOptions = mkOptionList
[ Option t' (t, d) t' [ Option t' (t, d) t'
@ -343,7 +336,6 @@ dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent),
, d <- [SortAsc, SortDesc] , d <- [SortAsc, SortDesc]
, let t' = CI.foldedCase t <> "-" <> toPathPiece d , let t' = CI.foldedCase t <> "-" <> toPathPiece d
] ]
(_, defPS) = runPSValidator dbtable Nothing
wIdent n wIdent n
| not $ null dbtIdent = dbtIdent <> "-" <> n | not $ null dbtIdent = dbtIdent <> "-" <> n
| otherwise = n | otherwise = n
@ -352,13 +344,13 @@ dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent),
| otherwise = dbsAttrs | otherwise = dbsAttrs
multiTextField = Field multiTextField = Field
{ fieldParse = \ts _ -> return . Right $ Just ts { fieldParse = \ts _ -> return . Right $ Just ts
, fieldView = undefined , fieldView = error "multiTextField: should not be rendered"
, fieldEnctype = UrlEncoded , fieldEnctype = UrlEncoded
} }
psResult <- runInputGetResult $ PaginationInput psResult <- runInputGetResult $ PaginationInput
<$> iopt (multiSelectField $ return sortingOptions) (wIdent "sorting") <$> iopt (multiSelectField $ return sortingOptions) (wIdent "sorting")
<*> ((assertM' $ not . Map.null) . Map.mapMaybe (assertM $ not . null) <$> Map.traverseWithKey (\k _ -> iopt multiTextField . wIdent $ CI.foldedCase k) dbtFilter) <*> (assertM' (not . Map.null) . Map.mapMaybe (assertM $ not . null) <$> Map.traverseWithKey (\k _ -> iopt multiTextField . wIdent $ CI.foldedCase k) dbtFilter)
<*> iopt intField (wIdent "pagesize") <*> iopt intField (wIdent "pagesize")
<*> iopt intField (wIdent "page") <*> iopt intField (wIdent "page")
<*> ireq checkBoxField (wIdent "table-only") <*> ireq checkBoxField (wIdent "table-only")
@ -373,7 +365,7 @@ dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent),
(errs, PaginationSettings{..}) = case psResult of (errs, PaginationSettings{..}) = case psResult of
FormSuccess pi FormSuccess pi
| not (piIsUnset pi) -> runPSValidator dbtable $ Just pi | not (piIsUnset pi) -> runPSValidator dbtable $ Just pi
FormFailure errs -> first (map SomeMessage errs <>) $ runPSValidator dbtable Nothing FormFailure errs' -> first (map SomeMessage errs' <>) $ runPSValidator dbtable Nothing
_ -> runPSValidator dbtable Nothing _ -> runPSValidator dbtable Nothing
psSorting' = map (first (dbtSorting !)) psSorting psSorting' = map (first (dbtSorting !)) psSorting
sqlQuery' = E.from $ \t -> dbtSQLQuery t sqlQuery' = E.from $ \t -> dbtSQLQuery t
@ -417,9 +409,9 @@ dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent),
wHeaders <- maybe (return Nothing) (fmap Just . genHeaders) pSortable wHeaders <- maybe (return Nothing) (fmap Just . genHeaders) pSortable
wRows <- forM rows $ \row -> forM (oneColonnadeEncode <$> getColonnade dbtColonnade) $ \(($ row) -> cell) -> do wRows <- forM rows $ \row' -> forM (oneColonnadeEncode <$> getColonnade dbtColonnade) $ \(($ row') -> cell') -> do
widget <- cell ^. cellContents widget <- cell' ^. cellContents
let attrs = cell ^. cellAttrs let attrs = cell' ^. cellAttrs
return $(widgetFile "table/cell/body") return $(widgetFile "table/cell/body")
let table = $(widgetFile "table/colonnade") let table = $(widgetFile "table/colonnade")
@ -467,7 +459,7 @@ cell :: IsDBTable m a => Widget -> DBCell m a
cell wgt = dbCell # ([], return wgt) cell wgt = dbCell # ([], return wgt)
textCell, stringCell :: (MonoFoldable msg, Element msg ~ Char, IsDBTable m a) => msg -> DBCell m a textCell, stringCell :: (MonoFoldable msg, Element msg ~ Char, IsDBTable m a) => msg -> DBCell m a
textCell = cell . toWidget . (pack :: [Char] -> Text) . otoList textCell = cell . toWidget . (pack :: String -> Text) . otoList
stringCell = textCell stringCell = textCell
i18nCell :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a i18nCell :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a
@ -480,7 +472,7 @@ tickmarkCell True = textCell (tickmark :: Text)
tickmarkCell False = mempty tickmarkCell False = mempty
cellTooltip :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a -> DBCell m a cellTooltip :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a -> DBCell m a
cellTooltip msg cell = cell & cellContents.mapped %~ (<> tipWdgt) cellTooltip msg = cellContents.mapped %~ (<> tipWdgt)
where where
tipWdgt = [whamlet| tipWdgt = [whamlet|
<div .js-tooltip> <div .js-tooltip>
@ -499,10 +491,10 @@ anchorCell' :: IsDBTable m a
-> (r -> DBCell m a) -> (r -> DBCell m a)
anchorCell' mkRoute mkWidget val = anchorCell (mkRoute val) (mkWidget val) anchorCell' mkRoute mkWidget val = anchorCell (mkRoute val) (mkWidget val)
anchorCellM :: IsDBTable m a => (WidgetT UniWorX IO (Route UniWorX)) -> Widget -> DBCell m a anchorCellM :: IsDBTable m a => WidgetT UniWorX IO (Route UniWorX) -> Widget -> DBCell m a
anchorCellM routeM widget = anchorCellM' routeM id (const widget) anchorCellM routeM widget = anchorCellM' routeM id (const widget)
anchorCellM' :: IsDBTable m a => (WidgetT UniWorX IO x) -> (x -> Route UniWorX) -> (x -> Widget) -> DBCell m a anchorCellM' :: IsDBTable m a => WidgetT UniWorX IO x -> (x -> Route UniWorX) -> (x -> Widget) -> DBCell m a
anchorCellM' xM x2route x2widget = cell $ do anchorCellM' xM x2route x2widget = cell $ do
x <- xM x <- xM
let route = x2route x let route = x2route x
@ -531,7 +523,7 @@ getDBFormResult initial (DBFormResult m) = Map.map (\(r, f) -> f $ initial r) m
formCell :: forall r i a. Ord i formCell :: forall r i a. Ord i
=> (r -> MForm (HandlerT UniWorX IO) i) => (r -> MForm (HandlerT UniWorX IO) i)
-> (r -> i -> MForm (HandlerT UniWorX IO) (FormResult (a -> a), Widget)) -> (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))) -> (r -> DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult (DBFormResult r i a)))
formCell genIndex genForm input = FormCell formCell genIndex genForm input = FormCell
{ formCellAttrs = [] { formCellAttrs = []
, formCellContents = do -- MForm (HandlerT UniWorX IO) (FormResult (Map i (Endo a)), Widget) , formCellContents = do -- MForm (HandlerT UniWorX IO) (FormResult (Map i (Endo a)), Widget)
@ -549,7 +541,7 @@ dbRow = Colonnade.singleton (headednessPure $ i18nCell MsgNrColumn) $ \DBRow{ db
dbSelect :: forall h r i a. (Headedness h, Ord i, PathPiece i) dbSelect :: forall h r i a. (Headedness h, Ord i, PathPiece i)
=> Setter' a Bool => Setter' a Bool
-> (r -> MForm (HandlerT UniWorX IO) i) -> (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))) -> 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 $ i18nCell MsgSelectColumn) $ \r -> flip (formCell genIndex) r $ \_ i -> do dbSelect resLens genIndex = Colonnade.singleton (headednessPure $ i18nCell MsgSelectColumn) $ \r -> flip (formCell genIndex) r $ \_ i -> do
(selResult, selWidget) <- mreq checkBoxField ("" { fsName = Just $ "select-" <> toPathPiece i }) (Just False) (selResult, selWidget) <- mreq checkBoxField ("" { fsName = Just $ "select-" <> toPathPiece i }) (Just False)
return (set resLens <$> selResult, [whamlet|^{fvInput selWidget}|]) return (set resLens <$> selResult, [whamlet|^{fvInput selWidget}|])

View File

@ -14,5 +14,5 @@ modal modalTrigger modalContent = do
triggerId <- newIdent triggerId <- newIdent
$(widgetFile "widgets/modal") $(widgetFile "widgets/modal")
case modalContent of case modalContent of
Left route -> [whamlet|<a .btn ##{triggerId} href=@{route}>^{modalTrigger}|] Left route -> [whamlet|<a .btn ##{triggerId} href=@{route}>^{modalTrigger}|]
Right content -> [whamlet|<div .btn ##{triggerId}>^{modalTrigger}|] Right _ -> [whamlet|<div .btn ##{triggerId}>^{modalTrigger}|]

View File

@ -57,7 +57,7 @@ consumeZip = unZipStream `fuseUpstream` consumeZip'
fileContent fileContent
| hasTrailingPathSeparator zipEntryName = Nothing | hasTrailingPathSeparator zipEntryName = Nothing
| otherwise = Just $ mconcat contentChunks | otherwise = Just $ mconcat contentChunks
yield $ File{..} yield File{..}
consumeZip' consumeZip'
accContents :: Monad m => Sink (Either a b) m [b] accContents :: Monad m => Sink (Either a b) m [b]
accContents = do accContents = do
@ -81,7 +81,7 @@ produceZip info = mapC toZipData =$= void (zipStream zipOptions)
} }
toZipData :: Monad m => File -> (ZipEntry, ZipData m) toZipData :: Monad m => File -> (ZipEntry, ZipData m)
toZipData f@(File{..}) = ((toZipEntry f){ zipEntrySize = fromIntegral . ByteString.length <$> fileContent }, maybe mempty (ZipDataByteString . Lazy.ByteString.fromStrict) fileContent) toZipData f@File{..} = ((toZipEntry f){ zipEntrySize = fromIntegral . ByteString.length <$> fileContent }, maybe mempty (ZipDataByteString . Lazy.ByteString.fromStrict) fileContent)
toZipEntry :: File -> ZipEntry toZipEntry :: File -> ZipEntry
toZipEntry File{..} = ZipEntry toZipEntry File{..} = ZipEntry

View File

@ -40,6 +40,8 @@ import Data.List.NonEmpty as Import (NonEmpty(..))
import Control.Monad.Morph as Import (MFunctor(..)) import Control.Monad.Morph as Import (MFunctor(..))
import Control.Monad.Trans.Resource as Import (ReleaseKey)
import Control.Monad.Trans.RWS (RWST) import Control.Monad.Trans.RWS (RWST)

View File

@ -2,6 +2,7 @@ module Jobs
( module Types ( module Types
, module Jobs.Queue , module Jobs.Queue
, handleJobs , handleJobs
, stopJobCtl
) where ) where
import Import import Import
@ -25,7 +26,7 @@ import Data.Semigroup (Max(..))
import Utils.Sql import Utils.Sql
import Control.Monad.Random (evalRand, mkStdGen) import Control.Monad.Random (evalRand, mkStdGen, getRandomR)
import Cron import Cron
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
@ -33,18 +34,18 @@ import Data.HashMap.Strict (HashMap)
import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map.Strict as Map
import Data.Foldable (foldrM) import Data.Foldable (foldrM)
import Control.Monad.Trans.Reader (mapReaderT) import Control.Monad.Trans.Reader (mapReaderT)
import Control.Monad.Trans.State (StateT, evalStateT, mapStateT) import Control.Monad.Trans.State (evalStateT, mapStateT)
import qualified Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import Control.Monad.Reader.Class (MonadReader(..)) import Control.Monad.Reader.Class (MonadReader(..))
import Control.Monad.Trans.Resource (MonadResourceBase, ResourceT, runResourceT, allocate) import Control.Monad.Trans.Resource (MonadResourceBase, runResourceT, allocate, release)
import Control.Monad.Trans.Maybe (MaybeT(..)) import Control.Monad.Trans.Maybe (MaybeT(..))
import Control.Monad.Logger import Control.Monad.Logger
import Control.Monad.Random (MonadRandom(..), evalRand)
import Data.Time.Clock import Data.Time.Clock
import Data.Time.Zones import Data.Time.Zones
@ -66,131 +67,171 @@ data JobQueueException = JInvalid QueuedJobId QueuedJob
instance Exception JobQueueException instance Exception JobQueueException
handleJobs :: (MonadResource m, MonadIO m) => [TMChan JobCtl] -> UniWorX -> m () handleJobs :: ( MonadResource m
-- | Read control commands from `appJobCtl` and address them as they come in , MonadIO m
)
=> UniWorX -> m ()
-- | Spawn a set of workers that read control commands from `appJobCtl` and address them as they come in
-- --
-- Uses `unsafeHandler`, as per documentation all HTTP-related fields of state/environment are meaningless placeholders. -- Uses `unsafeHandler`, as per documentation all HTTP-related fields of state/environment are meaningless placeholders.
-- Handling commands in `HandlerT` provides us with the facilities to render urls, unifies logging, provides a value of the foundation type, ... -- Handling commands in `HandlerT` provides us with the facilities to render urls, unifies logging, provides a value of the foundation type, ...
handleJobs recvChans foundation@UniWorX{..} = do handleJobs foundation@UniWorX{..} = do
jobCrontab <- liftIO $ newTVarIO HashMap.empty let num = appJobWorkers appSettings
jobCrontab <- liftIO $ newTMVarIO HashMap.empty
jobConfirm <- liftIO $ newTVarIO HashMap.empty jobConfirm <- liftIO $ newTVarIO HashMap.empty
forM_ (zip [1..] recvChans) $ \(n, chan) -> forM_ [1..num] $ \n -> do
(bChan, chan) <- atomically $ newBroadcastTMChan >>= (\c -> (c, ) <$> dupTMChan c)
let let
logStart = $logDebugS ("Jobs #" <> tshow n) "Starting" logStart = $logDebugS ("Jobs #" <> tshow n) "Starting"
logStop = $logDebugS ("Jobs #" <> tshow n) "Stopping" logStop = $logDebugS ("Jobs #" <> tshow n) "Stopping"
doFork = fork . unsafeHandler foundation . bracket_ logStart logStop . flip runReaderT JobContext{..} . runConduit $ sourceTMChan chan .| handleJobs' n removeChan = atomically . modifyTVar' appJobCtl . Map.delete =<< myThreadId
in void $ allocate (liftIO doFork) (\_ -> liftIO . atomically $ closeTMChan chan) doFork = flip forkFinally (\_ -> removeChan) . unsafeHandler foundation . bracket_ logStart logStop . flip runReaderT JobContext{..} . runConduit $ sourceTMChan chan .| handleJobs' n
(_, tId) <- allocate (liftIO doFork) (\_ -> liftIO . atomically $ closeTMChan chan)
atomically . modifyTVar' appJobCtl $ Map.insert tId bChan
-- Start cron operation -- Start cron operation
void $ allocate (liftIO . fork . unsafeHandler foundation $ runReaderT execCrontab JobContext{..}) (liftIO . killThread) registeredCron <- liftIO newEmptyTMVarIO
liftIO . unsafeHandler foundation . flip runReaderT JobContext{..} $ let execCrontab' = whenM (atomically $ readTMVar registeredCron) $
writeJobCtlBlock JobCtlDetermineCrontab unsafeHandler foundation $ runReaderT execCrontab JobContext{..}
unregister = atomically . whenM (fromMaybe False <$> tryReadTMVar registeredCron) . void $ tryTakeTMVar appCronThread
cData <- allocate (liftIO . forkFinally execCrontab' $ \_ -> unregister) (\_ -> liftIO . atomically . void $ tryTakeTMVar jobCrontab)
registeredCron' <- atomically $ do
registeredCron' <- tryPutTMVar appCronThread cData
registeredCron' <$ putTMVar registeredCron registeredCron'
when registeredCron' $
liftIO . unsafeHandler foundation . flip runReaderT JobContext{..} $
writeJobCtlBlock JobCtlDetermineCrontab
stopJobCtl :: MonadIO m => UniWorX -> m ()
-- ^ Stop all worker threads currently running
stopJobCtl UniWorX{appJobCtl, appCronThread} = do
mcData <- atomically $ tryReadTMVar appCronThread
whenIsJust mcData $ \(rKey, _) -> do
liftIO $ release rKey
atomically . guardM $ isEmptyTMVar appCronThread
wMap <- liftIO $ readTVarIO appJobCtl
atomically $ forM_ wMap closeTMChan
atomically $ do
wMap' <- readTVar appJobCtl
guard . none (`Map.member` wMap') $ Map.keysSet wMap
execCrontab :: ReaderT JobContext (HandlerT UniWorX IO) () execCrontab :: ReaderT JobContext (HandlerT UniWorX IO) ()
-- ^ Keeping a `HashMap` of the latest execution times of `JobCtl`s we have -- ^ Keeping a `HashMap` of the latest execution times of `JobCtl`s we have
-- seen, wait for the time of the next job and fire it -- seen, wait for the time of the next job and fire it
execCrontab = flip evalStateT HashMap.empty . forever $ do execCrontab = evalStateT go HashMap.empty
mapStateT (liftHandlerT . runDB . setSerializable) $ do
let
merge (Entity leId CronLastExec{..})
| Just job <- Aeson.parseMaybe parseJSON cronLastExecJob
= State.modify $ HashMap.insertWith (<>) (JobCtlQueue job) (Max cronLastExecTime)
| otherwise = lift $ delete leId
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ merge
now <- liftIO getCurrentTime
(currentCrontab, (jobCtl, nextMatch)) <- mapStateT (mapReaderT $ liftIO . atomically) $ do
crontab <- liftBase . readTVar =<< asks jobCrontab
State.modify . HashMap.filterWithKey $ \k _ -> HashMap.member k crontab
prevExec <- State.get
case earliestJob prevExec crontab now of
Nothing -> liftBase retry
Just (_, MatchNone) -> liftBase retry
Just x -> return (crontab, x)
let doJob = mapStateT (mapReaderT $ liftHandlerT . runDBJobs . setSerializable) $ do
newCrontab <- lift . lift . hoist lift $ determineCrontab'
if
| ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab
-> do
now <- liftIO $ getCurrentTime
instanceID <- getsYesod appInstanceID
State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl
case jobCtl of
JobCtlQueue job -> do
lift . lift $ upsertBy
(UniqueCronLastExec $ toJSON job)
CronLastExec
{ cronLastExecJob = toJSON job
, cronLastExecTime = now
, cronLastExecInstance = instanceID
}
[ CronLastExecTime =. now ]
lift . lift $ queueDBJob job
other -> writeJobCtl other
| otherwise
-> lift . mapReaderT (liftIO . atomically) $
lift . flip writeTVar newCrontab =<< asks jobCrontab
case nextMatch of
MatchAsap -> doJob
MatchNone -> return ()
MatchAt nextTime -> do
JobContext{jobCrontab} <- ask
nextTime' <- applyJitter jobCtl nextTime
$logDebugS "Cron" [st|Waiting until #{tshow (utcToLocalTimeTZ appTZ nextTime')} to execute #{tshow jobCtl}|]
logFunc <- askLoggerIO
whenM (liftIO . flip runLoggingT logFunc $ waitUntil jobCrontab currentCrontab nextTime')
doJob
where where
acc :: NominalDiffTime go = do
acc = 1e-3 mapStateT (liftHandlerT . runDB . setSerializable) $ do
let
merge (Entity leId CronLastExec{..})
| Just job <- Aeson.parseMaybe parseJSON cronLastExecJob
= State.modify $ HashMap.insertWith (<>) (JobCtlQueue job) (Max cronLastExecTime)
| otherwise = lift $ delete leId
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ merge
applyJitter :: (MonadHandler m, HandlerSite m ~ UniWorX, Hashable seed) => seed -> UTCTime -> m UTCTime refT <- liftIO getCurrentTime
applyJitter seed t = do currentState <- mapStateT (mapReaderT $ liftIO . atomically) $ do
appInstance <- getsYesod appInstanceID crontab' <- liftBase . tryReadTMVar =<< asks jobCrontab
let case crontab' of
halfRange = truncate $ 0.5 / acc Nothing -> return Nothing
diff = evalRand ( (* acc) . fromInteger <$> getRandomR (- halfRange, halfRange)) $ mkStdGen (hash appInstance `hashWithSalt` seed) Just crontab -> Just <$> do
return $ addUTCTime diff t State.modify . HashMap.filterWithKey $ \k _ -> HashMap.member k crontab
prevExec <- State.get
case earliestJob prevExec crontab refT of
Nothing -> liftBase retry
Just (_, MatchNone) -> liftBase retry
Just x -> return (crontab, x)
earliestJob :: HashMap JobCtl (Max UTCTime) -> Crontab JobCtl -> UTCTime -> Maybe (JobCtl, CronNextMatch UTCTime) case currentState of
earliestJob lastTimes crontab now = foldr go Nothing $ HashMap.toList crontab Nothing -> return ()
Just (currentCrontab, (jobCtl, nextMatch)) -> do
let doJob = mapStateT (mapReaderT $ liftHandlerT . runDBJobs . setSerializable) $ do
newCrontab <- lift . lift . hoist lift $ determineCrontab'
if
| ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab
-> do
now <- liftIO $ getCurrentTime
instanceID <- getsYesod appInstanceID
State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl
case jobCtl of
JobCtlQueue job -> do
void . lift . lift $ upsertBy
(UniqueCronLastExec $ toJSON job)
CronLastExec
{ cronLastExecJob = toJSON job
, cronLastExecTime = now
, cronLastExecInstance = instanceID
}
[ CronLastExecTime =. now ]
lift . lift $ queueDBJob job
other -> writeJobCtl other
| otherwise
-> lift . mapReaderT (liftIO . atomically) $
lift . void . flip swapTMVar newCrontab =<< asks jobCrontab
case nextMatch of
MatchAsap -> doJob
MatchNone -> return ()
MatchAt nextTime -> do
JobContext{jobCrontab} <- ask
nextTime' <- applyJitter jobCtl nextTime
$logDebugS "Cron" [st|Waiting until #{tshow (utcToLocalTimeTZ appTZ nextTime')} to execute #{tshow jobCtl}|]
logFunc <- askLoggerIO
whenM (liftIO . flip runLoggingT logFunc $ waitUntil jobCrontab currentCrontab nextTime')
doJob
go
where where
go (jobCtl, cron) mbPrev acc :: NominalDiffTime
| Just (_, t') <- mbPrev acc = 1e-3
, t' < t
= mbPrev applyJitter :: (MonadHandler m, HandlerSite m ~ UniWorX, Hashable seed) => seed -> UTCTime -> m UTCTime
| otherwise applyJitter seed t = do
= Just (jobCtl, t) appInstance <- getsYesod appInstanceID
let
halfRange = truncate $ 0.5 / acc
diff = evalRand ( (* acc) . fromInteger <$> getRandomR (- halfRange, halfRange)) $ mkStdGen (hash appInstance `hashWithSalt` seed)
return $ addUTCTime diff t
earliestJob :: HashMap JobCtl (Max UTCTime) -> Crontab JobCtl -> UTCTime -> Maybe (JobCtl, CronNextMatch UTCTime)
earliestJob lastTimes crontab now = foldr go' Nothing $ HashMap.toList crontab
where where
t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) now cron go' (jobCtl, cron) mbPrev
| Just (_, t') <- mbPrev
, t' < t
= mbPrev
| otherwise
= Just (jobCtl, t)
where
t = nextCronMatch appTZ (getMax <$> HashMap.lookup jobCtl lastTimes) now cron
waitUntil :: (Eq a, MonadResourceBase m, MonadLogger m) => TVar a -> a -> UTCTime -> m Bool waitUntil :: (Eq a, MonadResourceBase m, MonadLogger m) => TMVar a -> a -> UTCTime -> m Bool
waitUntil crontabTV crontab nextTime = runResourceT $ do waitUntil crontabTV crontab nextTime = runResourceT $ do
diffT <- diffUTCTime nextTime <$> liftIO getCurrentTime diffT <- diffUTCTime nextTime <$> liftIO getCurrentTime
let waitTime = fromInteger (truncate $ diffT / acc) * toRational acc let waitTime = fromInteger (truncate $ diffT / acc) * toRational acc
waitTime' waitTime'
| diffT < acc = "Done" | diffT < acc = "Done"
| otherwise = tshow (realToFrac waitTime :: NominalDiffTime) | otherwise = tshow (realToFrac waitTime :: NominalDiffTime)
$logDebugS "waitUntil" [st|#{tshow diffT} (#{waitTime'})|] $logDebugS "waitUntil" [st|#{tshow diffT} (#{waitTime'})|]
if if
| diffT < acc -> return True | diffT < acc -> return True
| otherwise -> do | otherwise -> do
retVar <- liftIO newEmptyTMVarIO retVar <- liftIO newEmptyTMVarIO
void $ allocate (liftIO $ forkFinally (threadDelay . round $ waitTime * 1e6) $ atomically . putTMVar retVar) (liftIO . killThread) void . liftIO . forkFinally (threadDelay . round $ waitTime * 1e6) $ atomically . putTMVar retVar
let let
awaitDelayThread = False <$ takeTMVar retVar awaitDelayThread = False <$ takeTMVar retVar
awaitCrontabChange = do awaitCrontabChange = do
crontab' <- readTVar crontabTV crontab' <- tryReadTMVar crontabTV
True <$ guard (crontab /= crontab') True <$ guard (Just crontab /= crontab')
crontabChanged <- liftIO . atomically $ awaitCrontabChange <|> awaitDelayThread crontabChanged <- liftIO . atomically $ awaitCrontabChange <|> awaitDelayThread
bool (waitUntil crontabTV crontab nextTime) (return False) crontabChanged bool (waitUntil crontabTV crontab nextTime) (return False) crontabChanged
handleJobs' :: Int -> Sink JobCtl (ReaderT JobContext Handler) () handleJobs' :: Natural -> Sink JobCtl (ReaderT JobContext Handler) ()
handleJobs' wNum = C.mapM_ $ \jctl -> do handleJobs' wNum = C.mapM_ $ \jctl -> do
$logDebugS logIdent $ tshow jctl $logDebugS logIdent $ tshow jctl
resVars <- mapReaderT (liftIO . atomically) $ resVars <- mapReaderT (liftIO . atomically) $
@ -228,7 +269,7 @@ handleJobs' wNum = C.mapM_ $ \jctl -> do
newCTab <- liftHandlerT . runDB $ setSerializable determineCrontab' newCTab <- liftHandlerT . runDB $ setSerializable determineCrontab'
-- $logDebugS logIdent $ tshow newCTab -- $logDebugS logIdent $ tshow newCTab
mapReaderT (liftIO . atomically) $ mapReaderT (liftIO . atomically) $
lift . flip writeTVar newCTab =<< asks jobCrontab lift . void . flip swapTMVar newCTab =<< asks jobCrontab
jLocked :: QueuedJobId -> (QueuedJob -> Handler a) -> Handler a jLocked :: QueuedJobId -> (QueuedJob -> Handler a) -> Handler a
jLocked jId act = do jLocked jId act = do

View File

@ -92,7 +92,7 @@ determineCrontab = execWriterT $ do
collateSubmissions :: [Entity Submission] -> Map UserId (Max (Maybe UTCTime)) collateSubmissions :: [Entity Submission] -> Map UserId (Max (Maybe UTCTime))
collateSubmissions = Map.fromListWith (<>) . fmap procCorrector collateSubmissions = Map.fromListWith (<>) . fmap procCorrector
where where
procCorrector :: Entity Submission -> (UserId , (Max (Maybe UTCTime))) procCorrector :: Entity Submission -> (UserId ,Max (Maybe UTCTime))
procCorrector = (,) <$> fromJust . submissionRatingBy . entityVal procCorrector = (,) <$> fromJust . submissionRatingBy . entityVal
<*> Max . submissionRatingAssigned . entityVal <*> Max . submissionRatingAssigned . entityVal

View File

@ -2,7 +2,7 @@ module Jobs.Handler.HelpRequest
( dispatchJobHelpRequest ( dispatchJobHelpRequest
) where ) where
import Import hiding ((.=)) import Import
import Text.Hamlet import Text.Hamlet
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI

View File

@ -1,10 +1,11 @@
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-} -- ihamletFile discards do results
module Jobs.Handler.SendNotification.CorrectionsAssigned module Jobs.Handler.SendNotification.CorrectionsAssigned
( dispatchNotificationCorrectionsAssigned ( dispatchNotificationCorrectionsAssigned
) where ) where
import Import import Import
import Utils.Lens
import Handler.Utils.Mail import Handler.Utils.Mail
import Text.Hamlet import Text.Hamlet
@ -25,10 +26,6 @@ dispatchNotificationCorrectionsAssigned nUser nSheet jRecipient = do
MsgRenderer mr <- getMailMsgRenderer MsgRenderer mr <- getMailMsgRenderer
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
tid = courseTerm
ssh = courseSchool
csh = courseShorthand
shn = sheetName
addAlternatives $ do addAlternatives $ do
let editNotifications = $(ihamletFile "templates/mail/editNotifications.hamlet") let editNotifications = $(ihamletFile "templates/mail/editNotifications.hamlet")

View File

@ -1,10 +1,11 @@
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-} -- ihamletFile discards do results
module Jobs.Handler.SendNotification.SheetActive module Jobs.Handler.SendNotification.SheetActive
( dispatchNotificationSheetActive ( dispatchNotificationSheetActive
) where ) where
import Import import Import
import Utils.Lens
import Handler.Utils.Mail import Handler.Utils.Mail
import Text.Hamlet import Text.Hamlet

View File

@ -1,3 +1,5 @@
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-} -- ihamletFile discards do results
module Jobs.Handler.SendNotification.SheetInactive module Jobs.Handler.SendNotification.SheetInactive
( dispatchNotificationSheetSoonInactive ( dispatchNotificationSheetSoonInactive
, dispatchNotificationSheetInactive , dispatchNotificationSheetInactive
@ -5,7 +7,6 @@ module Jobs.Handler.SendNotification.SheetInactive
import Import import Import
import Utils.Lens
import Handler.Utils.Mail import Handler.Utils.Mail
import Text.Hamlet import Text.Hamlet

View File

@ -1,3 +1,5 @@
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-} -- ihamletFile discards do results
module Jobs.Handler.SendNotification.SubmissionRated module Jobs.Handler.SendNotification.SubmissionRated
( dispatchNotificationSubmissionRated ( dispatchNotificationSubmissionRated
) where ) where
@ -40,7 +42,7 @@ dispatchNotificationSubmissionRated nSubmission jRecipient = userMailT jRecipien
, "submission-rating-comment" Aeson..= submissionRatingComment , "submission-rating-comment" Aeson..= submissionRatingComment
, "submission-rating-time" Aeson..= submissionRatingTime , "submission-rating-time" Aeson..= submissionRatingTime
, "submission-rating-by" Aeson..= (userDisplayName <$> corrector) , "submission-rating-by" Aeson..= (userDisplayName <$> corrector)
, "submission-rating-passed" Aeson..= (join $ gradingPassed <$> sheetType ^? _grading <*> submissionRatingPoints) , "submission-rating-passed" Aeson..= join (gradingPassed <$> sheetType ^? _grading <*> submissionRatingPoints)
, "sheet-name" Aeson..= sheetName , "sheet-name" Aeson..= sheetName
, "sheet-type" Aeson..= sheetType , "sheet-type" Aeson..= sheetType
, "course-name" Aeson..= courseName , "course-name" Aeson..= courseName

View File

@ -18,14 +18,24 @@ import qualified Data.Set as Set
import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Control.Monad.Random (MonadRandom(..), evalRand, mkStdGen, uniform) import Control.Monad.Random (evalRand, mkStdGen, uniform)
data JobQueueException = JobQueuePoolEmpty
deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic)
instance Exception JobQueueException
writeJobCtl :: (MonadHandler m, HandlerSite m ~ UniWorX) => JobCtl -> m () writeJobCtl :: (MonadHandler m, HandlerSite m ~ UniWorX) => JobCtl -> m ()
writeJobCtl cmd = do writeJobCtl cmd = do
tid <- liftIO myThreadId tid <- liftIO myThreadId
chan <- flip evalRand (mkStdGen (hash tid `hashWithSalt` cmd)) . uniform <$> getsYesod appJobCtl wMap <- getsYesod appJobCtl >>= liftIO . readTVarIO
liftIO . atomically $ writeTMChan chan cmd if
| null wMap -> throwM JobQueuePoolEmpty
| otherwise -> do
let chan = flip evalRand (mkStdGen (hash tid `hashWithSalt` cmd)) $ uniform wMap
liftIO . atomically $ writeTMChan chan cmd
writeJobCtlBlock :: (MonadHandler m, HandlerSite m ~ UniWorX) => JobCtl -> ReaderT JobContext m () writeJobCtlBlock :: (MonadHandler m, HandlerSite m ~ UniWorX) => JobCtl -> ReaderT JobContext m ()
writeJobCtlBlock cmd = do writeJobCtlBlock cmd = do
@ -72,6 +82,3 @@ runDBJobs act = do
(ret, jIds) <- liftHandlerT . runDB $ mapReaderT runWriterT act (ret, jIds) <- liftHandlerT . runDB $ mapReaderT runWriterT act
forM_ jIds $ writeJobCtl . JobCtlPerform forM_ jIds $ writeJobCtl . JobCtlPerform
return ret return ret

View File

@ -55,6 +55,6 @@ instance Hashable JobCtl
data JobContext = JobContext data JobContext = JobContext
{ jobCrontab :: TVar (Crontab JobCtl) { jobCrontab :: TMVar (Crontab JobCtl)
, jobConfirm :: TVar (HashMap JobCtl (NonEmpty (TMVar (Maybe SomeException)))) , jobConfirm :: TVar (HashMap JobCtl (NonEmpty (TMVar (Maybe SomeException))))
} }

View File

@ -32,14 +32,13 @@ module Mail
) where ) where
import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender) import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender)
import qualified ClassyPrelude.Yesod as Yesod (getMessageRender)
import Network.Mail.Mime hiding (addPart, addAttachment) import Network.Mail.Mime hiding (addPart, addAttachment)
import qualified Network.Mail.Mime as Mime (addPart) import qualified Network.Mail.Mime as Mime (addPart)
import Data.Monoid (Last(..)) import Data.Monoid (Last(..))
import Control.Monad.Trans.RWS (RWST(..), execRWST) import Control.Monad.Trans.RWS (RWST(..))
import Control.Monad.Trans.State (StateT(..), execStateT, State, mapStateT) import Control.Monad.Trans.State (StateT(..), execStateT, mapStateT)
import Control.Monad.Trans.Writer (execWriter, Writer) import Control.Monad.Trans.Writer (execWriter, Writer)
import Control.Monad.RWS.Class (MonadWriter(..), MonadReader(..), MonadState(..), modify) import Control.Monad.RWS.Class (MonadWriter(..), MonadReader(..), MonadState(..), modify)
import Control.Monad.Fail import Control.Monad.Fail
@ -50,8 +49,6 @@ import Generics.Deriving.Monoid (memptydefault, mappenddefault)
import Data.Sequence (Seq) import Data.Sequence (Seq)
import qualified Data.Sequence as Seq import qualified Data.Sequence as Seq
import Data.Data (Data)
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
@ -59,15 +56,13 @@ import qualified Data.Text as Text
import qualified Data.Foldable as Foldable import qualified Data.Foldable as Foldable
import Data.Hashable
import qualified Data.Text.Lazy as LT import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as LTB import qualified Data.Text.Lazy.Builder as LTB
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import Utils (MsgRendererS(..)) import Utils (MsgRendererS(..))
import Utils.Lens.TH import Utils.Lens.TH
import Control.Lens import Control.Lens hiding (from)
import Text.Blaze.Renderer.Utf8 import Text.Blaze.Renderer.Utf8
@ -84,7 +79,6 @@ import Network.BSD (getHostName)
import Data.Time.Zones (TZ, utcTZ, utcToLocalTimeTZ, timeZoneForUTCTime) import Data.Time.Zones (TZ, utcTZ, utcToLocalTimeTZ, timeZoneForUTCTime)
import Data.Time.LocalTime (ZonedTime(..)) import Data.Time.LocalTime (ZonedTime(..))
import Data.Time.Format
import Network.HaskellNet.SMTP (SMTPConnection) import Network.HaskellNet.SMTP (SMTPConnection)
import qualified Network.HaskellNet.SMTP as SMTP import qualified Network.HaskellNet.SMTP as SMTP
@ -96,7 +90,6 @@ import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Encode.Pretty as Aeson import qualified Data.Aeson.Encode.Pretty as Aeson
import Data.Aeson (Options(..)) import Data.Aeson (Options(..))
import Data.Aeson.TH import Data.Aeson.TH
import Utils (MsgRendererS(..))
import Utils.PathPiece (splitCamel) import Utils.PathPiece (splitCamel)
import Utils.DateTime import Utils.DateTime
@ -108,7 +101,7 @@ makeLenses_ ''Mail
makeLenses_ ''Part makeLenses_ ''Part
newtype MailT m a = MailT { unMailT :: RWST MailContext MailSmtpData Mail m a } newtype MailT m a = MailT { _unMailT :: RWST MailContext MailSmtpData Mail m a }
deriving newtype ( MonadTrans, Monad, Functor, MonadFail, Applicative, Alternative, MonadPlus deriving newtype ( MonadTrans, Monad, Functor, MonadFail, Applicative, Alternative, MonadPlus
, MonadIO, MonadHandler, MonadCatch, MonadThrow, MonadMask, MonadResource, MonadBase b , MonadIO, MonadHandler, MonadCatch, MonadThrow, MonadMask, MonadResource, MonadBase b
, MonadState Mail, MonadWriter MailSmtpData, MonadReader MailContext , MonadState Mail, MonadWriter MailSmtpData, MonadReader MailContext
@ -250,11 +243,11 @@ defMailT :: ( MonadHandler m
) => MailContext ) => MailContext
-> MailT m a -> MailT m a
-> m a -> m a
defMailT ls (MailT mail) = do defMailT ls (MailT mailC) = do
fromAddress <- defaultFromAddress fromAddress <- defaultFromAddress
(ret, mail, smtpData) <- runRWST mail ls (emptyMail fromAddress) (ret, mail, smtpData) <- runRWST mailC ls (emptyMail fromAddress)
mail' <- liftIO $ LBS.toStrict <$> renderMail' mail mail' <- liftIO $ LBS.toStrict <$> renderMail' mail
$logDebugS "Mail" $ "Rendered mail:\n" <> decodeUtf8 mail' -- $logDebugS "Mail" $ "Rendered mail:\n" <> decodeUtf8 mail'
ret <$ case smtpData of ret <$ case smtpData of
MailSmtpData{ smtpEnvelopeFrom = Last Nothing } -> throwM MailNoSenderSpecified MailSmtpData{ smtpEnvelopeFrom = Last Nothing } -> throwM MailNoSenderSpecified
MailSmtpData{ smtpRecipients } MailSmtpData{ smtpRecipients }
@ -457,12 +450,13 @@ setMailSmtpData = do
if if
| Verp{..} <- verpMode | Verp{..} <- verpMode
, [recp] <- Set.toList recps , [recp] <- Set.toList recps
-> let doVerp (Text.breakOn "@" -> (user, domain)) recp = mconcat -> let (user, domain) = Text.breakOn "@" from
verp = mconcat
[ user [ user
, Text.singleton verpSeparator , Text.singleton verpSeparator
, Text.replace "@" (Text.singleton verpAtReplacement) recp , Text.replace "@" (Text.singleton verpAtReplacement) recp
, domain , domain
] ]
in tell $ mempty { smtpEnvelopeFrom = Last . Just $ doVerp from recp } in tell $ mempty { smtpEnvelopeFrom = Last $ Just verp }
| otherwise | otherwise
-> tell $ mempty { smtpEnvelopeFrom = Last $ Just from } -> tell $ mempty { smtpEnvelopeFrom = Last $ Just from }

View File

@ -14,7 +14,6 @@ import Model.Types
import Cron.Types import Cron.Types
import Data.Aeson (Value) import Data.Aeson (Value)
import Data.Aeson.TH (deriveJSON, defaultOptions)
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import Data.CaseInsensitive.Instances () import Data.CaseInsensitive.Instances ()
@ -31,6 +30,7 @@ share [mkPersist sqlSettings, mkDeleteCascade sqlSettings, mkMigrate "migrateAll
-- (Eq Course) is impossible so we derive it for the Uniqueness Constraint only -- (Eq Course) is impossible so we derive it for the Uniqueness Constraint only
deriving instance Eq (Unique Course) deriving instance Eq (Unique Course)
deriving instance Eq (Unique Sheet)
submissionRatingDone :: Submission -> Bool submissionRatingDone :: Submission -> Bool
submissionRatingDone Submission{..} = isJust submissionRatingTime submissionRatingDone Submission{..} = isJust submissionRatingTime

View File

@ -75,15 +75,15 @@ migrateAll = do
Confusion about quotes, from the PostgreSQL Manual: Confusion about quotes, from the PostgreSQL Manual:
Single quotes for string constants, double quotes for table/column names. Single quotes for string constants, double quotes for table/column names.
QuasiQuoter: ^{TableName} @{ColumnName} (includes Escaping); QuasiQuoter: ^{TableName} @{ColumnName} (escaped as column/table-name; value determined from current model);
#{anything} (no escaping); #{anything} (escaped as value);
-} -}
customMigrations :: MonadIO m => Map (Key AppliedMigration) (ReaderT SqlBackend m ()) customMigrations :: MonadIO m => Map (Key AppliedMigration) (ReaderT SqlBackend m ())
customMigrations = Map.fromListWith (>>) customMigrations = Map.fromListWith (>>)
[ ( AppliedMigrationKey [migrationVersion|initial|] [version|0.0.0|] [ ( AppliedMigrationKey [migrationVersion|initial|] [version|0.0.0|]
, whenM (tableExists "user") $ do -- New theme format , whenM (columnExists "user" "theme") $ do -- New theme format
userThemes <- [sqlQQ| SELECT "id", "theme" FROM "user"; |] userThemes <- [sqlQQ| SELECT "id", "theme" FROM "user"; |]
forM_ userThemes $ \(uid, Single str) -> case stripPrefix "theme--" str of forM_ userThemes $ \(uid, Single str) -> case stripPrefix "theme--" str of
Just v Just v
@ -98,7 +98,7 @@ customMigrations = Map.fromListWith (>>)
|] |]
) )
, ( AppliedMigrationKey [migrationVersion|1.0.0|] [version|2.0.0|] , ( AppliedMigrationKey [migrationVersion|1.0.0|] [version|2.0.0|]
, whenM (tableExists "school") $ do -- SchoolId is the Shorthand CI Text now , whenM (columnExists "school" "id") $ do -- SchoolId is the Shorthand CI Text now
-- Read old table into memory -- Read old table into memory
schoolTable <- [sqlQQ| SELECT "id", "shorthand" FROM "school"; |] schoolTable <- [sqlQQ| SELECT "id", "shorthand" FROM "school"; |]
let _sT = schoolTable :: [(Single Int64, Single (CI Text))] -- Types needed let _sT = schoolTable :: [(Single Int64, Single (CI Text))] -- Types needed
@ -143,9 +143,9 @@ customMigrations = Map.fromListWith (>>)
FOREIGN KEY (school) REFERENCES school(shorthand); FOREIGN KEY (school) REFERENCES school(shorthand);
|] |]
[executeQQ| [executeQQ|
ALTER TABLE "school" DROP COLUMN "id"; ALTER TABLE "school" DROP COLUMN "id";
ALTER TABLE "school" ADD PRIMARY KEY (shorthand); ALTER TABLE "school" ADD PRIMARY KEY (shorthand);
|] |]
) )
, ( AppliedMigrationKey [migrationVersion|2.0.0|] [version|3.0.0|] , ( AppliedMigrationKey [migrationVersion|2.0.0|] [version|3.0.0|]
, whenM (tableExists "sheet_corrector") $ do -- Load is encoded as JSON now. , whenM (tableExists "sheet_corrector") $ do -- Load is encoded as JSON now.
@ -161,32 +161,31 @@ customMigrations = Map.fromListWith (>>)
, whenM (tableExists "user") $ do , whenM (tableExists "user") $ do
userDisplayNames <- [sqlQQ| SELECT "id", "display_name" FROM "user"; |] userDisplayNames <- [sqlQQ| SELECT "id", "display_name" FROM "user"; |]
[executeQQ| [executeQQ|
ALTER TABLE "user" ADD COLUMN "surname" text DEFAULT ''; ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "surname" text DEFAULT '';
|] |]
forM_ userDisplayNames $ \(uid, Single str) -> case lastMaybe $ words str of forM_ userDisplayNames $ \(uid, Single str) -> case lastMaybe $ words str of
Just name -> update uid [UserSurname =. name] Just name -> update uid [UserSurname =. name]
_other -> error $ "Empty userDisplayName found" _other -> error "Empty userDisplayName found"
) )
, ( AppliedMigrationKey [migrationVersion|3.1.0|] [version|3.2.0|] , ( AppliedMigrationKey [migrationVersion|3.1.0|] [version|3.2.0|]
, whenM (tableExists "sheet") $ do , whenM (tableExists "sheet") $
[executeQQ| [executeQQ|
ALTER TABLE "sheet" ADD COLUMN "upload_mode" json DEFAULT '{ "tag": "Upload", "unpackZips": true }'; ALTER TABLE "sheet" ADD COLUMN IF NOT EXISTS "upload_mode" json DEFAULT '{ "tag": "Upload", "unpackZips": true }';
|] |]
) )
, ( AppliedMigrationKey [migrationVersion|3.2.0|] [version|4.0.0|] , ( AppliedMigrationKey [migrationVersion|3.2.0|] [version|4.0.0|]
, whenM (tableExists "user") $ do , whenM (columnExists "user" "plugin") $
-- <> is standard sql for /= -- <> is standard sql for /=
[executeQQ| [executeQQ|
DELETE FROM "user" WHERE "plugin" <> 'LDAP'; DELETE FROM "user" WHERE "plugin" <> 'LDAP';
ALTER TABLE "user" DROP COLUMN "plugin"; ALTER TABLE "user" DROP COLUMN "plugin";
ALTER TABLE "user" ADD COLUMN "authentication" json DEFAULT '"ldap"'; ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "authentication" json DEFAULT '"ldap"';
|] |]
) )
, ( AppliedMigrationKey [migrationVersion|4.0.0|] [version|5.0.0|] , ( AppliedMigrationKey [migrationVersion|4.0.0|] [version|5.0.0|]
, whenM (tableExists "user") $ do , whenM (tableExists "user") $
[executeQQ| [executeQQ|
ALTER TABLE "user" ADD COLUMN "notification_settings" json DEFAULT null; ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "notification_settings" json NOT NULL DEFAULT '[]';
UPDATE "user" SET "notification_settings" = (#{def :: NotificationSettings} :: json) WHERE "notification_settings" is null;
|] |]
) )
, ( AppliedMigrationKey [migrationVersion|5.0.0|] [version|6.0.0|] , ( AppliedMigrationKey [migrationVersion|5.0.0|] [version|6.0.0|]
@ -200,7 +199,18 @@ customMigrations = Map.fromListWith (>>)
tableExists :: MonadIO m => Text -> ReaderT SqlBackend m Bool tableExists :: MonadIO m => Text -> ReaderT SqlBackend m Bool
tableExists table = do tableExists table = do
haveSchoolTable <- [sqlQQ| SELECT to_regclass(#{table}); |] haveTable <- [sqlQQ| SELECT to_regclass(#{table}); |]
case haveSchoolTable :: [Maybe (Single PersistValue)] of case haveTable :: [Maybe (Single PersistValue)] of
[Just _] -> return True [Just _] -> return True
_other -> return False _other -> return False
columnExists :: MonadIO m
=> Text -- ^ Table
-> Text -- ^ Column
-> ReaderT SqlBackend m Bool
columnExists table column = do
haveColumn <- [sqlQQ|SELECT column_name FROM information_schema.columns WHERE table_name=#{table} and column_name=#{column};|]
case haveColumn :: [Single PersistValue] of
[_] -> return True
_other -> return False

View File

@ -1,11 +1,7 @@
module Model.Migration.Types where module Model.Migration.Types where
import ClassyPrelude.Yesod hiding (derivePersistFieldJSON) import ClassyPrelude.Yesod
import qualified Data.Aeson as Aeson import Data.Aeson.TH (deriveJSON, defaultOptions)
import qualified Data.Aeson.Types as Aeson
import Data.Aeson (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), FromJSONKeyFunction(..), withText, withObject, Value())
import Data.Aeson.Types (toJSONKeyText)
import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..), SumEncoding(..))
import Database.Persist.Sql import Database.Persist.Sql
import qualified Model as Current import qualified Model as Current
@ -19,9 +15,9 @@ data SheetType
deriving (Show, Read, Eq) deriving (Show, Read, Eq)
sheetType :: SheetType -> Current.SheetType sheetType :: SheetType -> Current.SheetType
sheetType Bonus {..} = Current.Bonus $ Current.Points {..} sheetType Bonus {..} = Current.Bonus Current.Points {..}
sheetType Normal {..} = Current.Normal $ Current.Points {..} sheetType Normal {..} = Current.Normal Current.Points {..}
sheetType Pass {..} = Current.Normal $ Current.PassPoints {..} sheetType Pass {..} = Current.Normal Current.PassPoints {..}
sheetType NotGraded = Current.NotGraded sheetType NotGraded = Current.NotGraded
{- TODO: {- TODO:
@ -30,4 +26,4 @@ sheetType NotGraded = Current.NotGraded
deriveJSON defaultOptions ''SheetType deriveJSON defaultOptions ''SheetType
Current.derivePersistFieldJSON ''SheetType Current.derivePersistFieldJSON ''SheetType

View File

@ -64,22 +64,25 @@ instance PersistFieldSql Version where
version, migrationVersion :: QuasiQuoter version, migrationVersion :: QuasiQuoter
version = QuasiQuoter{..} version = undefinedQuote{quoteExp}
where where
quoteExp v = TH.lift $ case [ x | (x, "") <- readP_to_S parseVersion v] of quoteExp v = case [ x | (x, "") <- readP_to_S parseVersion v] of
[x] -> x [x] -> TH.lift x
[] -> error "No parse" [] -> error "No parse"
_ -> error "Ambiguous parse" _ -> error "Ambiguous parse"
quotePat = error "version cannot be used as pattern" migrationVersion = undefinedQuote{quoteExp}
quoteType = error "version cannot be used as type"
quoteDec = error "version cannot be used as declaration"
migrationVersion = QuasiQuoter{..}
where where
quoteExp "initial" = TH.lift InitialVersion quoteExp "initial" = TH.lift InitialVersion
quoteExp v = TH.lift $ case [ x | (x, "") <- readP_to_S parseVersion v] of quoteExp v = case [ x | (x, "") <- readP_to_S parseVersion v] of
[x] -> MigrationVersion x [x] -> TH.lift $ MigrationVersion x
[] -> error "No parse" [] -> error "No parse"
_ -> error "Ambiguous parse" _ -> error "Ambiguous parse"
undefinedQuote :: QuasiQuoter
undefinedQuote = QuasiQuoter{..}
where
quoteExp = error "version cannot be used as expression"
quotePat = error "version cannot be used as pattern" quotePat = error "version cannot be used as pattern"
quoteType = error "version cannot be used as type" quoteType = error "version cannot be used as type"
quoteDec = error "version cannot be used as declaration" quoteDec = error "version cannot be used as declaration"

View File

@ -16,10 +16,10 @@ import Language.Haskell.TH.Datatype
derivePersistFieldJSON :: Name -> DecsQ derivePersistFieldJSON :: Name -> DecsQ
derivePersistFieldJSON n = do derivePersistFieldJSON tName = do
DatatypeInfo{..} <- reifyDatatype n DatatypeInfo{..} <- reifyDatatype tName
vars <- forM datatypeVars (const $ newName "a") vars <- forM datatypeVars (const $ newName "a")
let t = foldl (\t n -> t `appT` varT n) (conT n) vars let t = foldl (\t' n' -> t' `appT` varT n') (conT tName) vars
iCxt iCxt
| null vars = cxt [] | null vars = cxt []
| otherwise = cxt [[t|ToJSON|] `appT` t, [t|FromJSON|] `appT` t] | otherwise = cxt [[t|ToJSON|] `appT` t, [t|FromJSON|] `appT` t]
@ -39,8 +39,8 @@ derivePersistFieldJSON n = do
bs <- newName "bs" bs <- newName "bs"
clause [[p|PersistByteString $(varP bs)|]] (normalB [e|first pack $ JSON.eitherDecodeStrict' $(varE bs)|]) [] clause [[p|PersistByteString $(varP bs)|]] (normalB [e|first pack $ JSON.eitherDecodeStrict' $(varE bs)|]) []
, do , do
t <- newName "t" text <- newName "text"
clause [[p|PersistText $(varP t)|]] (normalB [e|first pack . JSON.eitherDecodeStrict' $ Text.encodeUtf8 $(varE t)|]) [] clause [[p|PersistText $(varP text)|]] (normalB [e|first pack . JSON.eitherDecodeStrict' $ Text.encodeUtf8 $(varE text)|]) []
, clause [wildP] (normalB [e|Left "JSON values must be converted from PersistDbSpecific, PersistText, or PersistByteString"|]) [] , clause [wildP] (normalB [e|Left "JSON values must be converted from PersistDbSpecific, PersistText, or PersistByteString"|]) []
] ]
] ]

View File

@ -81,7 +81,7 @@ data AppSettings = AppSettings
, appMailObjectDomain :: Text , appMailObjectDomain :: Text
, appMailVerp :: VerpMode , appMailVerp :: VerpMode
, appMailSupport :: Address , appMailSupport :: Address
, appJobWorkers :: Int , appJobWorkers :: Natural
, appJobFlushInterval :: Maybe NominalDiffTime , appJobFlushInterval :: Maybe NominalDiffTime
, appJobCronInterval :: NominalDiffTime , appJobCronInterval :: NominalDiffTime
, appJobStaleThreshold :: NominalDiffTime , appJobStaleThreshold :: NominalDiffTime
@ -136,7 +136,7 @@ instance Show PWHashConf where
instance FromJSON PWHashConf where instance FromJSON PWHashConf where
parseJSON = withObject "PWHashConf" $ \o -> do parseJSON = withObject "PWHashConf" $ \o -> do
pwHashAlgorithm' <- (o .: "algorithm" :: Aeson.Parser Text) pwHashAlgorithm' <- o .: "algorithm" :: Aeson.Parser Text
pwHashAlgorithm <- if pwHashAlgorithm <- if
| pwHashAlgorithm' == "pbkdf1" -> return PWStore.pbkdf1 | pwHashAlgorithm' == "pbkdf1" -> return PWStore.pbkdf1
| pwHashAlgorithm' == "pbkdf2" -> return PWStore.pbkdf2 | pwHashAlgorithm' == "pbkdf2" -> return PWStore.pbkdf2

View File

@ -114,7 +114,7 @@ instance FromJSON ClientSession.Key where
instance ClusterSetting 'ClusterErrorMessageKey where instance ClusterSetting 'ClusterErrorMessageKey where
type ClusterSettingValue 'ClusterErrorMessageKey = SecretBox.Key type ClusterSettingValue 'ClusterErrorMessageKey = SecretBox.Key
initClusterSetting _ = liftIO $ SecretBox.newKey initClusterSetting _ = liftIO SecretBox.newKey
knownClusterSetting _ = ClusterErrorMessageKey knownClusterSetting _ = ClusterErrorMessageKey
instance ToJSON SecretBox.Key where instance ToJSON SecretBox.Key where

View File

@ -21,6 +21,7 @@ import Utils.DateTime as Utils
import Utils.PathPiece as Utils import Utils.PathPiece as Utils
import Utils.Message as Utils import Utils.Message as Utils
import Utils.Lang as Utils import Utils.Lang as Utils
import Control.Lens as Utils (none)
import Text.Blaze (Markup, ToMarkup) import Text.Blaze (Markup, ToMarkup)
@ -56,7 +57,7 @@ import qualified Data.Aeson as Aeson
-- Yesod -- -- Yesod --
----------- -----------
newtype MsgRendererS site = MsgRenderer { render :: (forall msg. RenderMessage site msg => msg -> Text) } newtype MsgRendererS site = MsgRenderer { render :: forall msg. RenderMessage site msg => msg -> Text }
getMsgRenderer :: forall m site. (MonadHandler m, HandlerSite m ~ site) => m (MsgRendererS site) getMsgRenderer :: forall m site. (MonadHandler m, HandlerSite m ~ site) => m (MsgRendererS site)
getMsgRenderer = do getMsgRenderer = do
@ -103,29 +104,29 @@ tickmarkT = tickmark
text2Html :: Text -> Html text2Html :: Text -> Html
text2Html = toHtml -- prevents ambiguous types text2Html = toHtml -- prevents ambiguous types
toWgt :: (ToMarkup a, MonadBaseControl IO m, MonadThrow m, MonadIO m) => toWgt :: (ToMarkup a, MonadBaseControl IO m, MonadThrow m, MonadIO m)
a -> WidgetT site m () => a -> WidgetT site m ()
toWgt = toWidget . toHtml toWgt = toWidget . toHtml
-- Convenience Functions to avoid type signatures: -- Convenience Functions to avoid type signatures:
text2widget :: (MonadBaseControl IO m, MonadThrow m, MonadIO m) => text2widget :: (MonadBaseControl IO m, MonadThrow m, MonadIO m)
Text -> WidgetT site m () => Text -> WidgetT site m ()
text2widget t = [whamlet|#{t}|] text2widget t = [whamlet|#{t}|]
citext2widget :: (MonadBaseControl IO m, MonadThrow m, MonadIO m) => citext2widget :: (MonadBaseControl IO m, MonadThrow m, MonadIO m)
(CI Text) -> WidgetT site m () => CI Text -> WidgetT site m ()
citext2widget t = [whamlet|#{CI.original t}|] citext2widget t = [whamlet|#{CI.original t}|]
str2widget :: (MonadBaseControl IO m, MonadThrow m, MonadIO m) => str2widget :: (MonadBaseControl IO m, MonadThrow m, MonadIO m)
String -> WidgetT site m () => String -> WidgetT site m ()
str2widget s = [whamlet|#{s}|] str2widget s = [whamlet|#{s}|]
display2widget :: (MonadBaseControl IO m, MonadThrow m, MonadIO m, DisplayAble a) => display2widget :: (MonadBaseControl IO m, MonadThrow m, MonadIO m, DisplayAble a)
a -> WidgetT site m () => a -> WidgetT site m ()
display2widget = text2widget . display display2widget = text2widget . display
withFragment :: Monad m => MForm m (a, WidgetT site IO ()) -> Markup -> MForm m (a, WidgetT site IO ()) withFragment :: Monad m => MForm m (a, WidgetT site IO ()) -> Markup -> MForm m (a, WidgetT site IO ())
withFragment form html = (flip fmap) form $ \(x, widget) -> (x, toWidget html >> widget) withFragment form html = flip fmap form $ over _2 (toWidget html >>)
-- Types that can be converted to Text for direct displayed to User! (Show for debugging, Display for Production) -- Types that can be converted to Text for direct displayed to User! (Show for debugging, Display for Production)
@ -173,7 +174,7 @@ instance {-# OVERLAPPABLE #-} Show a => DisplayAble a where -- The easy way out
-} -}
textPercent :: Double -> Text -- slow, maybe use Data.Double.Conversion.Text.toFixed instead? textPercent :: Double -> Text -- slow, maybe use Data.Double.Conversion.Text.toFixed instead?
textPercent x = lz <> (pack $ show rx) <> "%" textPercent x = lz <> pack (show rx) <> "%"
where where
round' :: Double -> Int -- avoids annoying warning round' :: Double -> Int -- avoids annoying warning
round' = round round' = round
@ -259,10 +260,10 @@ infixl 5 !!!
(!!!) :: (Ord k, Monoid v) => Map k v -> k -> v (!!!) :: (Ord k, Monoid v) => Map k v -> k -> v
(!!!) m k = (fromMaybe mempty) $ Map.lookup k m (!!!) m k = fromMaybe mempty $ Map.lookup k m
groupMap :: (Ord k, Ord v) => [(k,v)] -> Map k (Set v) groupMap :: (Ord k, Ord v) => [(k,v)] -> Map k (Set v)
groupMap l = Map.fromListWith mappend $ [(k, Set.singleton v) | (k,v) <- l] groupMap l = Map.fromListWith mappend [(k, Set.singleton v) | (k,v) <- l]
partMap :: (Ord k, Monoid v) => [(k,v)] -> Map k v partMap :: (Ord k, Monoid v) => [(k,v)] -> Map k v
partMap = Map.fromListWith mappend partMap = Map.fromListWith mappend
@ -367,19 +368,19 @@ whenIsRight (Left _) _ = return ()
maybeExceptT :: Monad m => e -> m (Maybe b) -> ExceptT e m b maybeExceptT :: Monad m => e -> m (Maybe b) -> ExceptT e m b
maybeExceptT err act = lift act >>= maybe (throwE err) return maybeExceptT err act = lift act >>= maybe (throwE err) return
maybeMExceptT :: Monad m => (m e) -> m (Maybe b) -> ExceptT e m b maybeMExceptT :: Monad m => m e -> m (Maybe b) -> ExceptT e m b
maybeMExceptT err act = lift act >>= maybe (lift err >>= throwE) return maybeMExceptT err act = lift act >>= maybe (lift err >>= throwE) return
whenExceptT :: Monad m => Bool -> e -> ExceptT e m () whenExceptT :: Monad m => Bool -> e -> ExceptT e m ()
whenExceptT b err = when b $ throwE err whenExceptT b err = when b $ throwE err
whenMExceptT :: Monad m => Bool -> (m e) -> ExceptT e m () whenMExceptT :: Monad m => Bool -> m e -> ExceptT e m ()
whenMExceptT b err = when b $ lift err >>= throwE whenMExceptT b err = when b $ lift err >>= throwE
guardExceptT :: Monad m => Bool -> e -> ExceptT e m () guardExceptT :: Monad m => Bool -> e -> ExceptT e m ()
guardExceptT b err = unless b $ throwE err guardExceptT b err = unless b $ throwE err
guardMExceptT :: Monad m => Bool -> (m e) -> ExceptT e m () guardMExceptT :: Monad m => Bool -> m e -> ExceptT e m ()
guardMExceptT b err = unless b $ lift err >>= throwE guardMExceptT b err = unless b $ lift err >>= throwE
exceptT :: Monad m => (e -> m b) -> (a -> m b) -> ExceptT e m a -> m b exceptT :: Monad m => (e -> m b) -> (a -> m b) -> ExceptT e m a -> m b
@ -397,9 +398,9 @@ catchIfMExceptT err p act = catchIf p (lift act) (throwE <=< lift . err)
shortCircuitM :: Monad m => (a -> Bool) -> m a -> m a -> (a -> a -> a) -> m a shortCircuitM :: Monad m => (a -> Bool) -> m a -> m a -> (a -> a -> a) -> m a
shortCircuitM sc mx my bop = do shortCircuitM sc mx my bop = do
x <- mx x <- mx
case sc x of if
True -> return x | sc x -> return x
False -> bop <$> pure x <*> my | otherwise -> bop <$> pure x <*> my
guardM :: MonadPlus m => m Bool -> m () guardM :: MonadPlus m => m Bool -> m ()
@ -434,7 +435,7 @@ allM xs f = andM $ fmap f xs
-- | Lazy monadic disjunction. -- | Lazy monadic disjunction.
or2M :: Monad m => m Bool -> m Bool -> m Bool or2M :: Monad m => m Bool -> m Bool -> m Bool
or2M ma mb = ifM ma (return True) mb or2M ma = ifM ma (return True)
orM :: (Foldable f, Monad m) => f (m Bool) -> m Bool orM :: (Foldable f, Monad m) => f (m Bool) -> m Bool
orM = Fold.foldr or2M (return False) orM = Fold.foldr or2M (return False)

View File

@ -24,11 +24,11 @@ entities2map = foldl' (\m entity -> Map.insert (entityKey entity) (entityVal ent
getKeyBy :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity record, PersistUniqueRead backend, MonadIO m) getKeyBy :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity record, PersistUniqueRead backend, MonadIO m)
=> Unique record -> ReaderT backend m (Maybe (Key record)) => Unique record -> ReaderT backend m (Maybe (Key record))
getKeyBy u = (fmap entityKey) <$> getBy u -- TODO optimize this, so that DB does not deliver entire record! getKeyBy u = fmap entityKey <$> getBy u -- TODO optimize this, so that DB does not deliver entire record!
getKeyBy404 :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity record, PersistUniqueRead backend, MonadIO m) getKeyBy404 :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity record, PersistUniqueRead backend, MonadIO m)
=> Unique record -> ReaderT backend m (Key record) => Unique record -> ReaderT backend m (Key record)
getKeyBy404 = (fmap entityKey) . getBy404 -- TODO optimize this, so that DB does not deliver entire record! getKeyBy404 = fmap entityKey . getBy404 -- TODO optimize this, so that DB does not deliver entire record!
existsBy :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity record, PersistUniqueRead backend, MonadIO m) existsBy :: (PersistEntityBackend record ~ BaseBackend backend, PersistEntity record, PersistUniqueRead backend, MonadIO m)
=> Unique record -> ReaderT backend m Bool => Unique record -> ReaderT backend m Bool

View File

@ -31,7 +31,7 @@ data FormLayout = FormStandard
renderAForm :: Monad m => FormLayout -> FormRender m a renderAForm :: Monad m => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do renderAForm formLayout aform fragment = do
(res, (($ []) -> views)) <- aFormToForm aform (res, ($ []) -> views) <- aFormToForm aform
let widget = $(widgetFile "widgets/form") let widget = $(widgetFile "widgets/form")
return (res, widget) return (res, widget)
@ -40,58 +40,58 @@ renderAForm formLayout aform fragment = do
-------------------- --------------------
fsl :: Text -> FieldSettings site fsl :: Text -> FieldSettings site
fsl lbl = fsl lbl
FieldSettings { fsLabel = (SomeMessage lbl) = FieldSettings { fsLabel = SomeMessage lbl
, fsTooltip = Nothing , fsTooltip = Nothing
, fsId = Nothing , fsId = Nothing
, fsName = Nothing , fsName = Nothing
, fsAttrs = [] , fsAttrs = []
} }
fslI :: RenderMessage site msg => msg -> FieldSettings site fslI :: RenderMessage site msg => msg -> FieldSettings site
fslI lbl = fslI lbl
FieldSettings { fsLabel = (SomeMessage lbl) = FieldSettings { fsLabel = SomeMessage lbl
, fsTooltip = Nothing , fsTooltip = Nothing
, fsId = Nothing , fsId = Nothing
, fsName = Nothing , fsName = Nothing
, fsAttrs = [] , fsAttrs = []
} }
fslp :: Text -> Text -> FieldSettings site fslp :: Text -> Text -> FieldSettings site
fslp lbl placeholder = fslp lbl placeholder
FieldSettings { fsLabel = (SomeMessage lbl) = FieldSettings { fsLabel = SomeMessage lbl
, fsTooltip = Nothing , fsTooltip = Nothing
, fsId = Nothing , fsId = Nothing
, fsName = Nothing , fsName = Nothing
, fsAttrs = [("placeholder", placeholder)] , fsAttrs = [("placeholder", placeholder)]
} }
fslpI :: RenderMessage site msg => msg -> Text -> FieldSettings site fslpI :: RenderMessage site msg => msg -> Text -> FieldSettings site
fslpI lbl placeholder = fslpI lbl placeholder
FieldSettings { fsLabel = (SomeMessage lbl) = FieldSettings { fsLabel = SomeMessage lbl
, fsTooltip = Nothing , fsTooltip = Nothing
, fsId = Nothing , fsId = Nothing
, fsName = Nothing , fsName = Nothing
, fsAttrs = [("placeholder", placeholder)] , fsAttrs = [("placeholder", placeholder)]
} }
addAttr :: Text -> Text -> FieldSettings site -> FieldSettings site addAttr :: Text -> Text -> FieldSettings site -> FieldSettings site
addAttr attr valu fs = fs { fsAttrs=newAttrs (fsAttrs fs) } addAttr attr valu fs = fs { fsAttrs = newAttrs $ fsAttrs fs }
where where
newAttrs :: [(Text,Text)] -> [(Text,Text)] newAttrs :: [(Text,Text)] -> [(Text,Text)]
newAttrs [] = [(attr,valu)] newAttrs [] = [(attr, valu)]
newAttrs (p@(a,v):t) newAttrs (p@(a,v) : t)
| attr==a = (a,T.append valu $ cons ' ' v):t | attr==a = (a, T.append valu $ cons ' ' v) : t
| otherwise = p:(newAttrs t) | otherwise = p : newAttrs t
addAttrs :: Text -> [Text] -> FieldSettings site -> FieldSettings site addAttrs :: Text -> [Text] -> FieldSettings site -> FieldSettings site
addAttrs attr valus fs = fs { fsAttrs=newAttrs (fsAttrs fs) } addAttrs attr valus fs = fs { fsAttrs = newAttrs $ fsAttrs fs }
where where
newAttrs :: [(Text,Text)] -> [(Text,Text)] newAttrs :: [(Text, Text)] -> [(Text, Text)]
newAttrs [] = [(attr,T.intercalate " " valus)] newAttrs [] = [(attr, T.intercalate " " valus)]
newAttrs (p@(a,v):t) newAttrs (p@(a,v) : t)
| attr==a = (a,T.intercalate " " (v:valus)):t | attr==a = ( a, T.intercalate " " $ v : valus ) : t
| otherwise = p:(newAttrs t) | otherwise = p : newAttrs t
addClass :: Text -> FieldSettings site -> FieldSettings site addClass :: Text -> FieldSettings site -> FieldSettings site
addClass = addAttr "class" addClass = addAttr "class"
@ -103,17 +103,19 @@ addName :: Text -> FieldSettings site -> FieldSettings site
addName nm fs = fs { fsName = Just nm } addName nm fs = fs { fsName = Just nm }
addNameClass :: Text -> Text -> FieldSettings site -> FieldSettings site addNameClass :: Text -> Text -> FieldSettings site -> FieldSettings site
addNameClass gName gClass fs = fs { fsName= Just gName, fsAttrs=("class",gClass):(fsAttrs fs) } addNameClass gName gClass fs = fs { fsName = Just gName, fsAttrs = ("class",gClass) : fsAttrs fs }
addIdClass :: Text -> Text -> FieldSettings site -> FieldSettings site addIdClass :: Text -> Text -> FieldSettings site -> FieldSettings site
addIdClass gId gClass fs = fs { fsId= Just gId, fsAttrs=("class",gClass):(fsAttrs fs) } addIdClass gId gClass fs = fs { fsId = Just gId, fsAttrs = ("class",gClass) : fsAttrs fs }
setClass :: FieldSettings site -> Text -> FieldSettings site -- deprecated setClass :: FieldSettings site -> Text -> FieldSettings site -- deprecated
setClass fs c = fs { fsAttrs=("class",c):(fsAttrs fs) } setClass fs c = fs { fsAttrs = ("class",c) : fsAttrs fs }
setNameClass :: FieldSettings site -> Text -> Text -> FieldSettings site -- deprecated setNameClass :: FieldSettings site -> Text -> Text -> FieldSettings site -- deprecated
setNameClass fs gName gClass = fs { fsName= Just gName, fsAttrs=("class",gClass):(fsAttrs fs) } setNameClass fs gName gClass = fs { fsName = Just gName
, fsAttrs = ("class",gClass) : fsAttrs fs
}
setTooltip :: RenderMessage site msg => msg -> FieldSettings site -> FieldSettings site setTooltip :: RenderMessage site msg => msg -> FieldSettings site -> FieldSettings site
setTooltip msg fs = fs { fsTooltip = Just $ SomeMessage msg } setTooltip msg fs = fs { fsTooltip = Just $ SomeMessage msg }
@ -179,7 +181,7 @@ identForm = identifyForm . toPathPiece
data family ButtonCssClass site :: * data family ButtonCssClass site :: *
bcc2txt :: Show (ButtonCssClass site) => ButtonCssClass site -> Text -- a Hack; maybe define Read/Show manually bcc2txt :: Show (ButtonCssClass site) => ButtonCssClass site -> Text -- a Hack; maybe define Read/Show manually
bcc2txt bcc = T.pack $ "btn-" ++ (Char.toLower <$> (drop 2 $ show bcc)) bcc2txt bcc = T.pack $ "btn-" ++ (Char.toLower <$> drop 2 (show bcc))
class (Enum a, Bounded a, Ord a, PathPiece a) => Button site a where class (Enum a, Bounded a, Ord a, PathPiece a) => Button site a where
label :: a -> WidgetT site IO () label :: a -> WidgetT site IO ()
@ -213,7 +215,7 @@ buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
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] -> AForm (HandlerT site IO) [Maybe a]
combinedButtonField btns = traverse b2f btns combinedButtonField = traverse b2f
where where
b2f b = aopt (buttonField b) "" Nothing b2f b = aopt (buttonField b) "" Nothing
@ -247,7 +249,7 @@ reorderField optList = Field{..}
olNum = fromIntegral $ length olOptions olNum = fromIntegral $ length olOptions
selOptions = Map.fromList $ do selOptions = Map.fromList $ do
i <- [1..olNum] i <- [1..olNum]
(readMay -> Just (n :: Word), ('.' : extVal)) <- break (== '.') . unpack <$> optlist (readMay -> Just (n :: Word), '.' : extVal) <- break (== '.') . unpack <$> optlist
guard $ i == n guard $ i == n
Just val <- return . olReadExternal $ pack extVal Just val <- return . olReadExternal $ pack extVal
return (i, val) return (i, val)

View File

@ -24,9 +24,7 @@ selectLanguage' avL (l:ls)
| not $ null l | not $ null l
, Just lParts <- NonEmpty.nonEmpty $ Text.splitOn "-" l , Just lParts <- NonEmpty.nonEmpty $ Text.splitOn "-" l
, found <- find ((NonEmpty.toList lParts `isPrefixOf`) . Text.splitOn "-") avL , found <- find ((NonEmpty.toList lParts `isPrefixOf`) . Text.splitOn "-") avL
= case found of = flip fromMaybe found $ selectLanguage' avL $ Text.intercalate "-" (NonEmpty.tail lParts) : ls
Just l' -> l'
Nothing -> selectLanguage' avL $ Text.intercalate "-" (NonEmpty.tail lParts) : ls
| otherwise = selectLanguage' avL ls | otherwise = selectLanguage' avL ls
langMatches :: Lang -- ^ Needle langMatches :: Lang -- ^ Needle

View File

@ -2,7 +2,7 @@ module Utils.Lens ( module Utils.Lens ) where
import Import.NoFoundation import Import.NoFoundation
import Control.Lens as Utils.Lens import Control.Lens as Utils.Lens
import Utils.Lens.TH import Utils.Lens.TH as Utils.Lens (makeLenses_)
import qualified Database.Esqueleto as E (Value(..),InnerJoin(..)) import qualified Database.Esqueleto as E (Value(..),InnerJoin(..))

View File

@ -6,7 +6,7 @@ import ClassyPrelude.Yesod
import Database.Persist.Sql import Database.Persist.Sql
import Database.PostgreSQL.Simple (sqlErrorHint) import Database.PostgreSQL.Simple (SqlError(SqlError), sqlErrorHint)
import Control.Monad.Catch (handleIf) import Control.Monad.Catch (handleIf)
import Data.Time.Clock import Data.Time.Clock
@ -18,7 +18,7 @@ setSerializable act = setSerializable' (0 :: Integer)
setSerializable' (min 10 -> logBackoff) = setSerializable' (min 10 -> logBackoff) =
handleIf handleIf
(\e -> "The transaction might succeed if retried." `isInfixOf` sqlErrorHint e) (\SqlError{sqlErrorHint} -> "The transaction might succeed if retried." `isInfixOf` sqlErrorHint)
(\e -> do (\e -> do
let let
delay :: NominalDiffTime delay :: NominalDiffTime

View File

@ -38,4 +38,6 @@ extra-deps:
- saltine-0.1.0.1 - saltine-0.1.0.1
- hlint-test-0.1.0.0
resolver: lts-10.5 resolver: lts-10.5

View File

@ -3,7 +3,7 @@
<tr .table__row> <tr .table__row>
<th .table__th>_{MsgSubmission} <th .table__th>_{MsgSubmission}
<td .table__td>#{display cid} <td .table__td>#{display cid}
$maybe Entity _ User{..} <- corrector $maybe Entity _ User{userDisplayName} <- corrector
<tr .table__row> <tr .table__row>
<th .table__th>_{MsgRatingBy} <th .table__th>_{MsgRatingBy}
<td .table__td>#{display userDisplayName} <td .table__td>#{display userDisplayName}

View File

@ -10,7 +10,7 @@
<!-- breadcrumbs --> <!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute $if not $ Just HomeR == mcurrentRoute
^{breadcrumbs} ^{breadcrumbsWgt}
<div .main__content-body> <div .main__content-body>

View File

@ -1,4 +1,4 @@
$forall AuthPlugin{..} <- plugins $forall AuthPlugin{apName, apLogin} <- plugins
$if apName == "LDAP" $if apName == "LDAP"
<section> <section>
<h2>_{MsgLDAPLoginTitle} <h2>_{MsgLDAPLoginTitle}

View File

@ -23,7 +23,7 @@ $newline never
<dd> <dd>
<a href=@{CSubmissionR tid ssh csh shn csid SubShowR}> <a href=@{CSubmissionR tid ssh csh shn csid SubShowR}>
#{display csid} #{display csid}
$maybe User{..} <- corrector $maybe User{userDisplayName} <- corrector
<dt> <dt>
_{MsgRatingBy} _{MsgRatingBy}
<dd> <dd>

View File

@ -12,7 +12,7 @@ $newline never
$of Left Nothing $of Left Nothing
$of Right Nothing $of Right Nothing
<dt>Ungültige UserId erhalten! <dt>Ungültige UserId erhalten!
$of Right (Just (Entity _ User{..})) $of Right (Just (Entity _ User{userDisplayName, userSurname, userIdent, userEmail, userMatrikelnummer, userMailLanguages}))
<dt>Name <dt>Name
<dd>^{const (const (nameHtml userDisplayName userSurname))} <dd>^{const (const (nameHtml userDisplayName userSurname))}
<dt>Identifikation <dt>Identifikation

View File

@ -1,6 +1,6 @@
_{MsgSubmissionFilesIgnored} _{MsgSubmissionFilesIgnored}
<ul> <ul>
$forall ident <- ignored $forall ident <- ignoredFiles
$case ident $case ident
$of Right fileTitle $of Right fileTitle
<li style="font-family: monospace">#{fileTitle} <li style="font-family: monospace">#{fileTitle}

View File

@ -1,16 +1,12 @@
$newline never $newline never
<aside .main__aside> <aside .main__aside>
<div .asidenav> <div .asidenav>
$forall tid@TermIdentifier{..} <- favouriteTerms $forall tid <- favouriteTerms
<div .asidenav__box.js-show-hide> <div .asidenav__box.js-show-hide>
<h3 .asidenav__box-title.js-show-hide__toggle data-sh-index="#{display season}-#{year}"> <h3 .asidenav__box-title.js-show-hide__toggle data-sh-index="#{termToText tid}">
$case season _{ShortTermIdentifier tid}
$of Winter
_{MsgWinterTermShort year}
$of Summer
_{MsgSummerTermShort year}
<ul .asidenav__list.js-show-hide__target.list--iconless> <ul .asidenav__list.js-show-hide__target.list--iconless>
$forall (Course{..}, courseRoute, pageActions) <- favouriteTerm tid $forall (Course{courseShorthand, courseName}, courseRoute, pageActions) <- favouriteTerm tid
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active> <li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
<a .asidenav__link-wrapper href=@{courseRoute}> <a .asidenav__link-wrapper href=@{courseRoute}>
<div .asidenav__link-shorthand>#{courseShorthand} <div .asidenav__link-shorthand>#{courseShorthand}
@ -18,7 +14,7 @@ $newline never
<ul .asidenav__nested-list.list--iconless> <ul .asidenav__nested-list.list--iconless>
$forall action <- pageActions $forall action <- pageActions
$case action $case action
$of PageActionPrime (MenuItem{..}) $of PageActionPrime (MenuItem{menuItemRoute, menuItemLabel})
<li .asidenav__nested-list-item> <li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=@{menuItemRoute}>#{menuItemLabel} <a .asidenav__link-wrapper href=@{menuItemRoute}>#{menuItemLabel}
$of _ $of _

View File

@ -1,2 +1,2 @@
<form method=POST action=@{toMaster $ PluginR "LDAP" []} enctype=#{loginEnctype}> <form method=POST action=@{toMaster $ PluginR "LDAP" []} enctype=#{loginEnctype} #login--campus>
^{login} ^{login}

View File

@ -0,0 +1,27 @@
<form .form-inline method=post action=@{ProfileDataR} enctype=#{btnEnctype}>
<h2>
Sind Sie sich absolut sicher, alle Ihre in Uni2work gespeicherten Daten zu löschen?
<div .container>
Während der Testphase von Uni2work können Sie hiermit
Ihren Account bei Uni2work vollständig löschen.
Mit Ihrem Campus-Account können Sie sich aber danach
jederzeit erneut einloggen, wodurch wieder ein leerer Account erstellt wird.
<div .container>
Hochgeladene Hausaufgaben-Dateien werden unabhhängig vom Urherber nur dann gelöscht,
wenn die Dateien ausschließlich Ihnen zugeordnet sind.
Dateien aus Gruppenabgaben werden also erst dann gelöscht,
wenn alle Gruppenmitglieder Ihren Account gelöscht haben.
<div .container>
<em>Achtung:
Auch abgegebene Hausübungen werden gelöscht!
Falls ein Veranstalter Informationen darüber nicht anderweitig gespeichert hat,
kann dadurch ein etwaiger Hausaufgabenbonus verloren gehen.
(Verbuchte Noten sollten dadurch nicht betroffen sein, aber in einem etwaigen
Streitfall konnen die per Uni2work verwalteten Hausaufgaben dann
auch nicht mehr rekonstruiert/berücksichtigt werden.)
<div .container>
<em>Nach der Testphase von Uni2work wird das Löschen eines Accounts etwas
eingeschränkt werden, da z.B. Klausurnoten 5 Jahre bis nach Exmatrikulation
aufbewahrt werden müssen.
<div .container>
^{btnWdgt}

View File

@ -1,2 +1,2 @@
<form method=POST action=@{toMaster $ PluginR "dummy" []} enctype=#{loginEnctype}> <form method=POST action=@{toMaster $ PluginR "dummy" []} enctype=#{loginEnctype} #login--dummy>
^{login} ^{login}

View File

@ -1,2 +1,2 @@
<form method=POST action=@{toMaster $ PluginR "PWHash" []} enctype=#{loginEnctype}> <form method=POST action=@{toMaster $ PluginR "PWHash" []} enctype=#{loginEnctype} #login--hash>
^{login} ^{login}

View File

@ -6,7 +6,7 @@ $maybe points <- submissionRatingPoints
$case grading $case grading
$of Points{..} $of Points{..}
_{MsgAchievedOf points maxPoints} _{MsgAchievedOf points maxPoints}
$of PassPoints{..} $of PassPoints{}
$if fromMaybe False (gradingPassed grading points) $if fromMaybe False (gradingPassed grading points)
_{MsgPassed} _{MsgPassed}
$else $else

View File

@ -1,38 +1,41 @@
$with realGrades <- normalSummary <> bonusSummary $with realGrades <- normalSummary <> bonusSummary
$with allGrades <- realGrades <> informationalSummary $# $with allGrades <- realGrades <> informationalSummary
<div> <div>
<ul> <ul>
$maybe realPoints <- positiveSum (sumGradePoints realGrades) $maybe realPoints <- positiveSum (sumGradePoints realGrades)
<li> <li>
Gesamtpunktzahl #{display realPoints} Gesamtpunktzahl #{display realPoints}
$maybe nPts <- getSum <$> achievedPoints realGrades $maybe nPts <- getSum <$> achievedPoints realGrades
\ davon #{display nPts} erreicht \ davon #{display nPts} erreicht
$maybe bPts <- getSum <$> achievedPoints bonusSummary $maybe bPts <- getSum <$> achievedPoints bonusSummary
\ (inklusive #{display bPts} # \ (inklusive #{display bPts} #
$maybe achievedBonus <- positiveSum (sumGradePoints bonusSummary) $maybe achievedBonus <- positiveSum (sumGradePoints bonusSummary)
von #{display achievedBonus} erreichbaren # von #{display achievedBonus} erreichbaren #
Bonuspunkten) Bonuspunkten)
$if realPoints /= 0
\ #{textPercent $ realToFrac $ nPts / realPoints} \ #{textPercent $ realToFrac $ nPts / realPoints}
$maybe fakePoints <- positiveSum (sumGradePoints informationalSummary) \.
<li> $maybe fakePoints <- positiveSum (sumGradePoints informationalSummary)
<em>Hinweis: <li>
\ #{display fakePoints} Punkte gab es für Aufgabenblätter, # <em>Hinweis:
welche nicht gewertet wurden, sondern nur informativen Charakter besitzen \ #{display fakePoints} Punkte gab es für Aufgabenblätter, #
$maybe achievedFakes <- getSum <$> achievedPoints informationalSummary welche nicht gewertet wurden, sondern nur informativen Charakter besitzen
, davon wurden #{display achievedFakes} erreicht $maybe achievedFakes <- getSum <$> achievedPoints informationalSummary
, davon wurden #{display achievedFakes} erreicht
$if fakePoints /= 0
\ #{textPercent $ realToFrac $ achievedFakes / fakePoints} \ #{textPercent $ realToFrac $ achievedFakes / fakePoints}
. \.
$maybe reqPasses <- positiveSum (numGradePasses normalSummary) $maybe reqPasses <- positiveSum (numGradePasses normalSummary)
<li> <li>
Aufgaben zum Bestehen: #{display reqPasses} Aufgaben zum Bestehen: #{display reqPasses}
$maybe passed <- getSum <$> achievedPasses realGrades $maybe passed <- getSum <$> achievedPasses realGrades
\ davon #{display passed} bestanden \ davon #{display passed} bestanden
$maybe bonusPassed <- getSum <$> achievedPasses bonusSummary $maybe bonusPassed <- getSum <$> achievedPasses bonusSummary
\ (inklusive #{display bonusPassed} Bonusaufgaben) \ (inklusive #{display bonusPassed} Bonusaufgaben)
. \.
$maybe noGradeSheets <- positiveSum numNotGraded $maybe noGradeSheets <- positiveSum numNotGraded
<li> <li>
#{display noGradeSheets} unbewertete Aufgabenblätter. #{display noGradeSheets} unbewertete Aufgabenblätter.

View File

@ -11,4 +11,4 @@ if [[ -d .stack-work-test ]]; then
trap move-back EXIT trap move-back EXIT
fi fi
stack test --flag uniworx:dev --flag uniworx:library-only ${@} stack build --test --fast --flag uniworx:dev --flag uniworx:library-only ${@}

View File

@ -5,7 +5,6 @@ module CronSpec where
import TestImport import TestImport
import Cron import Cron
import Numeric.Natural
import Data.Time import Data.Time
import Data.Time.Clock.System import Data.Time.Clock.System
@ -22,9 +21,9 @@ sampleCron :: Natural -> Cron -> [UTCTime]
sampleCron n = go n baseTime Nothing sampleCron n = go n baseTime Nothing
where where
go 0 _ _ _ = [] go 0 _ _ _ = []
go n t mPrev cron = case nextCronMatch utcTZ mPrev t cron of go (pred -> n') t mPrev cron = case nextCronMatch utcTZ mPrev t cron of
MatchAsap -> t : go (pred n) t (Just t) cron MatchAsap -> t : go n' t (Just t) cron
MatchAt t' -> t' : go (pred n) t' (Just t') cron MatchAt t' -> t' : go n' t' (Just t') cron
MatchNone -> [] MatchNone -> []
@ -32,8 +31,8 @@ spec :: Spec
spec = do spec = do
describe "Cron" $ do describe "Cron" $ do
it "generates correct example series" . mapM_ seriesExample $ it "generates correct example series" . mapM_ seriesExample $
[ (Cron CronAsap Nothing CronScheduleBefore, [baseTime]) [ (Cron CronAsap CronRepeatNever 0 (Right CronNotScheduled), [baseTime])
, (Cron CronAsap (Just $ CronPeriod 10 CronAsap) CronScheduleBefore, iterate (addUTCTime 10) baseTime) , (Cron CronAsap (CronRepeatScheduled CronAsap) 10 (Right CronNotScheduled), iterate (addUTCTime 10) baseTime)
] ]
seriesExample :: (Cron, [UTCTime]) -> Expectation seriesExample :: (Cron, [UTCTime]) -> Expectation

View File

@ -6,30 +6,11 @@ import TestImport
spec :: Spec spec :: Spec
spec = withApp $ do spec = withApp $ do
describe "Homepage" $ do describe "Homepage" $ do
it "loads the index and checks it looks right" $ do it "loads the index and checks it looks right" $ do
get HomeR
statusIs 200
htmlAnyContain "h1" "a modern framework for blazing fast websites"
request $ do request $ do
setMethod "POST" setMethod "GET"
setUrl HomeR setUrl HomeR
addToken addRequestHeader ("Accept-Language", "de")
fileByLabel "Choose a file" "test/Spec.hs" "text/plain" -- talk about self-reference
byLabel "What's on the file?" "Some Content"
statusIs 200 statusIs 200
-- more debugging printBody htmlAnyContain "h1" "Aktuelle Termine"
htmlAllContain ".upload-response" "text/plain"
htmlAllContain ".upload-response" "Some Content"
-- This is a simple example of using a database access in a test. The
-- test will succeed for a fresh scaffolded site with an empty database,
-- but will fail on an existing database with a non-empty user table.
it "leaves the user table empty" $ do
get HomeR
statusIs 200
users <- runDB $ selectList ([] :: [Filter User]) []
assertEq "user table empty" 0 $ length users

View File

@ -6,25 +6,40 @@ import TestImport
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Yesod.Core.Handler (toTextUrl)
import Yesod.Core.Unsafe (fakeHandlerGetLogger)
spec :: Spec spec :: Spec
spec = withApp $ do spec = withApp $ do
describe "Profile page" $ do
it "asserts no access to my-account for anonymous users" $ do
get ProfileR
describe "Profile page" $ do app <- getTestYesod
it "asserts no access to my-account for anonymous users" $ do loginText <- fakeHandlerGetLogger appLogger app (toTextUrl $ AuthR LoginR)
get ProfileR
statusIs 403
it "asserts access to my-account for authenticated users" $ do assertHeader "Location" $ encodeUtf8 loginText
userEntity <- createUser "foo"
authenticateAs userEntity either (fail . unpack) (\_ -> return ()) =<< followRedirect
statusIs 200
get ProfileR it "asserts access to my-account for authenticated users" $ do
statusIs 200 userEntity <- createUser "foo"
authenticateAs userEntity
it "asserts user's information is shown" $ do get ProfileR
userEntity <- createUser "bar" statusIs 200
authenticateAs userEntity
get ProfileR it "displays basic user data" $ do
let (Entity _ user) = userEntity userEntity@(Entity _userId User{..}) <- createUser "foo"
htmlAnyContain ".username" . unpack . CI.original $ userIdent user authenticateAs userEntity
get ProfileDataR
statusIs 200
forM_ (words userDisplayName) $ \nameWord -> do
htmlAnyContain ".profile dd" $ unpack nameWord
htmlAnyContain ".profile dd" $ unpack userSurname
htmlAnyContain ".profile dd" . unpack $ CI.original userIdent
htmlAnyContain ".profile dd" . unpack $ CI.original userEmail

View File

@ -23,6 +23,7 @@ instance Arbitrary File where
fileModified <- addUTCTime <$> arbitrary <*> pure (UTCTime date 0) fileModified <- addUTCTime <$> arbitrary <*> pure (UTCTime date 0)
fileContent <- arbitrary fileContent <- arbitrary
return File{..} return File{..}
shrink = genericShrink
spec :: Spec spec :: Spec
spec = describe "Zip file handling" $ do spec = describe "Zip file handling" $ do
@ -31,7 +32,7 @@ spec = describe "Zip file handling" $ do
zipFiles' <- runConduit $ Conduit.sourceList zipFiles =$= produceZip def =$= void consumeZip =$= Conduit.consume zipFiles' <- runConduit $ Conduit.sourceList zipFiles =$= produceZip def =$= void consumeZip =$= Conduit.consume
forM_ (zipFiles `zip` zipFiles') $ \(file, file') -> do forM_ (zipFiles `zip` zipFiles') $ \(file, file') -> do
let acceptableFilenameChanges let acceptableFilenameChanges
= makeValid . bool (dropWhileEnd isPathSeparator) addTrailingPathSeparator (isNothing $ fileContent file) . normalise . makeValid = makeValid . dropWhile isPathSeparator . bool (dropWhileEnd isPathSeparator) addTrailingPathSeparator (isNothing $ fileContent file) . normalise . makeValid
acceptableTimeDifference t1 t2 = abs (diffUTCTime t1 t2) <= 2 acceptableTimeDifference t1 t2 = abs (diffUTCTime t1 t2) <= 2
(shouldBe `on` acceptableFilenameChanges) (fileTitle file') (fileTitle file) (shouldBe `on` acceptableFilenameChanges) (fileTitle file') (fileTitle file)
when (inZipRange $ fileModified file) $ when (inZipRange $ fileModified file) $

View File

@ -14,7 +14,7 @@ instance Arbitrary Season where
instance Arbitrary TermIdentifier where instance Arbitrary TermIdentifier where
arbitrary = do arbitrary = do
season <- arbitrary season <- arbitrary
year <- arbitrary year <- arbitrary `suchThat` (\y -> abs y >= 100)
return $ TermIdentifier{..} return $ TermIdentifier{..}
shrink = genericShrink shrink = genericShrink
@ -24,8 +24,9 @@ spec = do
it "has compatible encoding/decoding to/from Text" . property $ it "has compatible encoding/decoding to/from Text" . property $
\term -> termFromText (termToText term) == Right term \term -> termFromText (termToText term) == Right term
it "works for some examples" . mapM_ termExample $ it "works for some examples" . mapM_ termExample $
[ (TermIdentifier 2017 Summer, "S2017") [ (TermIdentifier 2017 Summer, "S17")
, (TermIdentifier 1995 Winter, "W1995") , (TermIdentifier 1995 Winter, "W95")
, (TermIdentifier 3068 Winter, "W3068")
] ]
termExample :: (TermIdentifier, Text) -> Expectation termExample :: (TermIdentifier, Text) -> Expectation

View File

@ -1,8 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module TestImport module TestImport
( module TestImport ( module TestImport
, module X , module X
@ -11,11 +6,10 @@ module TestImport
import Application (makeFoundation, makeLogWare) import Application (makeFoundation, makeLogWare)
import ClassyPrelude as X hiding (delete, deleteBy, Handler) import ClassyPrelude as X hiding (delete, deleteBy, Handler)
import Database.Persist as X hiding (get) import Database.Persist as X hiding (get)
import Database.Persist.Sql (SqlPersistM, SqlBackend, runSqlPersistMPool, rawExecute, rawSql, unSingle, connEscapeName) import Database.Persist.Sql (SqlPersistM, runSqlPersistMPool, rawExecute, unSingle, connEscapeName, sqlQQ)
import Foundation as X import Foundation as X
import Model as X import Model as X
import Test.Hspec as X import Test.Hspec as X
import Text.Shakespeare.Text (st)
import Yesod.Default.Config2 (useEnv, loadYamlSettings) import Yesod.Default.Config2 (useEnv, loadYamlSettings)
import Yesod.Auth as X import Yesod.Auth as X
import Yesod.Test as X import Yesod.Test as X
@ -23,8 +17,12 @@ import Yesod.Core.Unsafe (fakeHandlerGetLogger)
import Test.QuickCheck as X import Test.QuickCheck as X
import Test.QuickCheck.Gen as X import Test.QuickCheck.Gen as X
import Data.Default as X import Data.Default as X
import Test.QuickCheck.Instances as X import Test.QuickCheck.Instances as X ()
import System.IO as X (hPrint, hPutStrLn, stderr)
import Jobs (handleJobs, stopJobCtl)
import Control.Monad.Trans.Resource (runResourceT, MonadResourceBase)
import Data.Pool (destroyAllResources)
import Settings import Settings
@ -34,60 +32,61 @@ import qualified Data.CaseInsensitive as CI
runDB :: SqlPersistM a -> YesodExample UniWorX a runDB :: SqlPersistM a -> YesodExample UniWorX a
runDB query = do runDB query = do
app <- getTestYesod app <- getTestYesod
liftIO $ runDBWithApp app query liftIO $ runDBWithApp app query
runDBWithApp :: UniWorX -> SqlPersistM a -> IO a runDBWithApp :: MonadIO m => UniWorX -> SqlPersistM a -> m a
runDBWithApp app query = runSqlPersistMPool query (appConnPool app) runDBWithApp app query = liftIO $ runSqlPersistMPool query (appConnPool app)
runHandler :: Handler a -> YesodExample UniWorX a runHandler :: Handler a -> YesodExample UniWorX a
runHandler handler = do runHandler handler = do
app <- getTestYesod app <- getTestYesod
fakeHandlerGetLogger appLogger app handler fakeHandlerGetLogger appLogger app handler
withApp :: SpecWith (TestApp UniWorX) -> Spec withApp :: YSpec UniWorX -> Spec
withApp = before $ do withApp = around $ \act -> runResourceT $ do
settings <- loadYamlSettings settings <- liftIO $ loadYamlSettings
["config/test-settings.yml", "config/settings.yml"] ["config/test-settings.yml", "config/settings.yml"]
[] []
useEnv useEnv
foundation <- makeFoundation settings foundation <- makeFoundation settings
wipeDB foundation let
logWare <- liftIO $ makeLogWare foundation stopDBAccess = do
return (foundation, logWare) stopJobCtl foundation
liftIO . destroyAllResources $ appConnPool foundation
bracket_ stopDBAccess (handleJobs foundation) $ wipeDB foundation
logWare <- makeLogWare foundation
lift $ act (foundation, logWare)
-- This function will truncate all of the tables in your database. -- This function will truncate all of the tables in your database.
-- 'withApp' calls it before each test, creating a clean environment for each -- 'withApp' calls it before each test, creating a clean environment for each
-- spec to run in. -- spec to run in.
wipeDB :: UniWorX -> IO () wipeDB :: (MonadResourceBase m, MonadMask m) => UniWorX -> m ()
wipeDB app = runDBWithApp app $ do wipeDB app = runDBWithApp app $ do
tables <- getTables tables <- map unSingle <$> [sqlQQ|SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'|]
sqlBackend <- ask sqlBackend <- ask
let escapedTables = map (connEscapeName sqlBackend . DBName) tables let escapedTables = map (connEscapeName sqlBackend . DBName) $ filter (not . (`elem` protected)) tables
query = "TRUNCATE TABLE " ++ intercalate ", " escapedTables query = "TRUNCATE TABLE " ++ intercalate ", " escapedTables ++ " RESTART IDENTITY"
rawExecute query [] protected = ["applied_migration"]
rawExecute query []
getTables :: MonadIO m => ReaderT SqlBackend m [Text]
getTables = do
tables <- rawSql [st|
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public';
|] []
return $ map unSingle tables
-- | Authenticate as a user. This relies on the `auth-dummy-login: true` flag -- | Authenticate as a user. This relies on the `auth-dummy-login: true` flag
-- being set in test-settings.yaml, which enables dummy authentication in -- being set in test-settings.yaml, which enables dummy authentication in
-- Foundation.hs -- Foundation.hs
authenticateAs :: Entity User -> YesodExample UniWorX () authenticateAs :: Entity User -> YesodExample UniWorX ()
authenticateAs (Entity _ User{..}) = do authenticateAs (Entity _ User{..}) = do
request $ do request $ do
setMethod "POST" setMethod "GET"
addPostParam "ident" $ CI.original userIdent addRequestHeader ("Accept-Language", "de")
setUrl $ AuthR $ PluginR "dummy" [] setUrl $ AuthR LoginR
request $ do
setMethod "POST"
addToken_ "#login--dummy"
byLabelExact "Nutzer-Kennung" $ CI.original userIdent
setUrl $ AuthR $ PluginR "dummy" []
-- | Create a user. The dummy email entry helps to confirm that foreign-key -- | Create a user. The dummy email entry helps to confirm that foreign-key
-- checking is switched off in wipeDB for those database backends which need it. -- checking is switched off in wipeDB for those database backends which need it.
@ -106,4 +105,6 @@ createUser userIdent = do
userDateFormat = userDefaultDateFormat userDateFormat = userDefaultDateFormat
userTimeFormat = userDefaultTimeFormat userTimeFormat = userDefaultTimeFormat
userDownloadFiles = userDefaultDownloadFiles userDownloadFiles = userDefaultDownloadFiles
userMailLanguages = def
userNotificationSettings = def
runDB $ insertEntity User{..} runDB $ insertEntity User{..}