Time formatting in emails
This commit is contained in:
parent
f98939885b
commit
d743fd6536
5
db.hs
5
db.hs
@ -83,6 +83,7 @@ fillDb = do
|
|||||||
, userDateFormat = userDefaultDateFormat
|
, userDateFormat = userDefaultDateFormat
|
||||||
, userTimeFormat = userDefaultTimeFormat
|
, userTimeFormat = userDefaultTimeFormat
|
||||||
, userDownloadFiles = userDefaultDownloadFiles
|
, userDownloadFiles = userDefaultDownloadFiles
|
||||||
|
, userMailLanguages = MailLanguages ["en"]
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
}
|
}
|
||||||
fhamann <- insert User
|
fhamann <- insert User
|
||||||
@ -98,6 +99,7 @@ fillDb = do
|
|||||||
, userDateFormat = userDefaultDateFormat
|
, userDateFormat = userDefaultDateFormat
|
||||||
, userTimeFormat = userDefaultTimeFormat
|
, userTimeFormat = userDefaultTimeFormat
|
||||||
, userDownloadFiles = userDefaultDownloadFiles
|
, userDownloadFiles = userDefaultDownloadFiles
|
||||||
|
, userMailLanguages = MailLanguages ["de"]
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
}
|
}
|
||||||
jost <- insert User
|
jost <- insert User
|
||||||
@ -113,6 +115,7 @@ fillDb = do
|
|||||||
, userDateFormat = userDefaultDateFormat
|
, userDateFormat = userDefaultDateFormat
|
||||||
, userTimeFormat = userDefaultTimeFormat
|
, userTimeFormat = userDefaultTimeFormat
|
||||||
, userDownloadFiles = userDefaultDownloadFiles
|
, userDownloadFiles = userDefaultDownloadFiles
|
||||||
|
, userMailLanguages = MailLanguages ["de"]
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
}
|
}
|
||||||
void . insert $ User
|
void . insert $ User
|
||||||
@ -128,6 +131,7 @@ fillDb = do
|
|||||||
, userDateFormat = userDefaultDateFormat
|
, userDateFormat = userDefaultDateFormat
|
||||||
, userTimeFormat = userDefaultTimeFormat
|
, userTimeFormat = userDefaultTimeFormat
|
||||||
, userDownloadFiles = userDefaultDownloadFiles
|
, userDownloadFiles = userDefaultDownloadFiles
|
||||||
|
, userMailLanguages = MailLanguages ["de"]
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
}
|
}
|
||||||
void . insert $ User
|
void . insert $ User
|
||||||
@ -143,6 +147,7 @@ fillDb = do
|
|||||||
, userDateFormat = userDefaultDateFormat
|
, userDateFormat = userDefaultDateFormat
|
||||||
, userTimeFormat = userDefaultTimeFormat
|
, userTimeFormat = userDefaultTimeFormat
|
||||||
, userDownloadFiles = userDefaultDownloadFiles
|
, userDownloadFiles = userDefaultDownloadFiles
|
||||||
|
, userMailLanguages = MailLanguages ["de"]
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
}
|
}
|
||||||
void . repsert (TermKey summer2017) $ Term
|
void . repsert (TermKey summer2017) $ Term
|
||||||
|
|||||||
@ -314,9 +314,23 @@ MailTestFormLanguages: Spracheinstellungen
|
|||||||
|
|
||||||
MailTestSubject: Uni2Work Test-Email
|
MailTestSubject: Uni2Work Test-Email
|
||||||
MailTestContent: Dies ist eine Test-Email versandt von Uni2Work. Von Ihrer Seite ist keine Handlung notwendig.
|
MailTestContent: Dies ist eine Test-Email versandt von Uni2Work. Von Ihrer Seite ist keine Handlung notwendig.
|
||||||
|
MailTestDateTime: Test der Datumsformattierung:
|
||||||
|
|
||||||
German: Deutsch
|
German: Deutsch
|
||||||
GermanGermany: Deutsch (Deutschland)
|
GermanGermany: Deutsch (Deutschland)
|
||||||
|
|
||||||
MailSubjectSubmissionRated csh@CourseShorthand: Ihre #{csh}-Abgabe wurde bewertet
|
MailSubjectSubmissionRated csh@CourseShorthand: Ihre #{csh}-Abgabe wurde bewertet
|
||||||
MailSubmissionRatedIntro courseName@Text termDesc@Text: Ihre Abgabe im Kurs #{courseName} (#{termDesc}) wurde bewertet.
|
MailSubmissionRatedIntro courseName@Text termDesc@Text: Ihre Abgabe im Kurs #{courseName} (#{termDesc}) wurde bewertet.
|
||||||
|
|
||||||
|
SheetTypeBonus: Bonus
|
||||||
|
SheetTypeNormal: Normal
|
||||||
|
SheetTypePass: Bestehen
|
||||||
|
SheetTypeNotGraded: Keine Wertung
|
||||||
|
|
||||||
|
SheetTypeMaxPoints: Maximalpunktzahl
|
||||||
|
SheetTypePassingPoints: Notwendig zum Bestehen
|
||||||
|
|
||||||
|
SheetGroupArbitrary: Arbiträre Gruppen
|
||||||
|
SheetGroupRegisteredGroups: Registrierte Gruppen
|
||||||
|
SheetGroupNoGroups: Keine Gruppenabgabe
|
||||||
|
SheetGroupMaxGroupsize: Maximale Gruppengröße
|
||||||
16
src/Data/Universe/Instances/Reverse/Hashable.hs
Normal file
16
src/Data/Universe/Instances/Reverse/Hashable.hs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{-# LANGUAGE NoImplicitPrelude
|
||||||
|
, ScopedTypeVariables
|
||||||
|
#-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Data.Universe.Instances.Reverse.Hashable
|
||||||
|
(
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude
|
||||||
|
|
||||||
|
import Data.Universe
|
||||||
|
|
||||||
|
|
||||||
|
instance (Hashable a, Hashable b, Finite a) => Hashable (a -> b) where
|
||||||
|
hashWithSalt s f = s `hashWithSalt` [ (k, f k) | k <- universeF ]
|
||||||
30
src/Data/Universe/Instances/Reverse/JSON.hs
Normal file
30
src/Data/Universe/Instances/Reverse/JSON.hs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{-# LANGUAGE NoImplicitPrelude
|
||||||
|
, ScopedTypeVariables
|
||||||
|
#-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Data.Universe.Instances.Reverse.JSON
|
||||||
|
(
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude
|
||||||
|
|
||||||
|
import Data.Aeson
|
||||||
|
import Data.Aeson.Types (Parser)
|
||||||
|
|
||||||
|
import qualified Data.HashSet as HashSet
|
||||||
|
import qualified Data.HashMap.Strict as HashMap
|
||||||
|
import Data.HashMap.Strict ((!))
|
||||||
|
|
||||||
|
import Data.Universe
|
||||||
|
|
||||||
|
|
||||||
|
instance (Eq a, Hashable a, Finite a, ToJSON b, ToJSONKey a) => ToJSON (a -> b) where
|
||||||
|
toJSON f = toJSON $ HashMap.fromList [(k, f k) | k <- universeF]
|
||||||
|
|
||||||
|
instance (Eq a, Hashable a, Finite a, FromJSON b, FromJSONKey a) => FromJSON (a -> b) where
|
||||||
|
parseJSON val = do
|
||||||
|
vMap <- parseJSON val :: Parser (HashMap a b)
|
||||||
|
unless (HashSet.fromMap (HashMap.map (const ()) vMap) == HashSet.fromList universeF) $
|
||||||
|
fail "Not all required keys found"
|
||||||
|
return $ (vMap !)
|
||||||
@ -1325,7 +1325,7 @@ instance YesodMail UniWorX where
|
|||||||
mailSmtp act = do
|
mailSmtp act = do
|
||||||
pool <- maybe (throwM MailNotAvailable) return =<< getsYesod appSmtpPool
|
pool <- maybe (throwM MailNotAvailable) return =<< getsYesod appSmtpPool
|
||||||
withResource pool act
|
withResource pool act
|
||||||
mailT ls mail = defMailT ls $ do
|
mailT ctx mail = defMailT ctx $ do
|
||||||
setMailObjectId
|
setMailObjectId
|
||||||
setDateCurrent
|
setDateCurrent
|
||||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
|
||||||
module Handler.Admin where
|
module Handler.Admin where
|
||||||
|
|
||||||
@ -44,11 +45,23 @@ instance Button UniWorX CreateButton where
|
|||||||
cssClass CreateInf = BCPrimary
|
cssClass CreateInf = BCPrimary
|
||||||
-- END Button needed here
|
-- END Button needed here
|
||||||
|
|
||||||
emailTestForm :: AForm (HandlerT UniWorX IO) (Email, MailLanguages)
|
emailTestForm :: AForm (HandlerT UniWorX IO) (Email, MailContext)
|
||||||
emailTestForm = (,)
|
emailTestForm = (,)
|
||||||
<$> areq emailField (fslI MsgMailTestFormEmail) Nothing
|
<$> areq emailField (fslI MsgMailTestFormEmail) Nothing
|
||||||
<*> (MailLanguages <$> areq (reorderField appLanguages) (fslI MsgMailTestFormLanguages) Nothing)
|
<*> ( MailContext
|
||||||
|
<$> (MailLanguages <$> areq (reorderField appLanguages) (fslI MsgMailTestFormLanguages) Nothing)
|
||||||
|
<*> (toMailDateTimeFormat
|
||||||
|
<$> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) Nothing
|
||||||
|
<*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) Nothing
|
||||||
|
<*> areq (selectField $ dateTimeFormatOptions SelFormatTime) (fslI MsgTimeFormat) Nothing
|
||||||
|
)
|
||||||
|
)
|
||||||
<* submitButton
|
<* submitButton
|
||||||
|
where
|
||||||
|
toMailDateTimeFormat dt d t = \case
|
||||||
|
SelFormatDateTime -> dt
|
||||||
|
SelFormatDate -> d
|
||||||
|
SelFormatTime -> t
|
||||||
|
|
||||||
|
|
||||||
getAdminTestR, postAdminTestR :: Handler Html -- Demo Page. Referenzimplementierungen sollte hier gezeigt werden!
|
getAdminTestR, postAdminTestR :: Handler Html -- Demo Page. Referenzimplementierungen sollte hier gezeigt werden!
|
||||||
@ -71,6 +84,11 @@ postAdminTestR = do
|
|||||||
writeJobCtl $ JobCtlPerform jId
|
writeJobCtl $ JobCtlPerform jId
|
||||||
FormMissing -> return ()
|
FormMissing -> return ()
|
||||||
(FormFailure errs) -> forM_ errs $ addMessage Error . toHtml
|
(FormFailure errs) -> forM_ errs $ addMessage Error . toHtml
|
||||||
|
|
||||||
|
let emailWidget' = [whamlet|
|
||||||
|
<form method=post action=@{AdminTestR} enctype=#{emailEnctype}>
|
||||||
|
^{emailWidget}
|
||||||
|
|]
|
||||||
|
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
-- setTitle "Uni2work Admin Testpage"
|
-- setTitle "Uni2work Admin Testpage"
|
||||||
|
|||||||
@ -254,7 +254,7 @@ correctionsR whereClause (formColonnade -> displayColumns) psValidator actions =
|
|||||||
tableForm <- makeCorrectionsTable whereClause displayColumns psValidator
|
tableForm <- makeCorrectionsTable whereClause displayColumns psValidator
|
||||||
((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
|
(actionRes, action) <- multiAction actions Nothing
|
||||||
return ((,) <$> actionRes <*> selectionRes, table <> action)
|
return ((,) <$> actionRes <*> selectionRes, table <> action)
|
||||||
|
|
||||||
Just currentRoute <- getCurrentRoute -- This should never be called from a 404 handler
|
Just currentRoute <- getCurrentRoute -- This should never be called from a 404 handler
|
||||||
|
|||||||
@ -72,7 +72,7 @@ makeSubmissionForm msmid uploadMode grouping buddies = identForm FIDsubmission $
|
|||||||
flip (renderAForm FormStandard) html $ (,)
|
flip (renderAForm FormStandard) html $ (,)
|
||||||
<$> fileUpload
|
<$> fileUpload
|
||||||
<*> (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 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
|
||||||
])
|
])
|
||||||
<* submitButton
|
<* submitButton
|
||||||
@ -215,7 +215,7 @@ submissionHelper tid ssh csh shn (SubmissionMode mcid) = do
|
|||||||
(Just (_,False,_)) -> pure . mr $ MsgNotAParticipant email tid csh
|
(Just (_,False,_)) -> pure . mr $ MsgNotAParticipant email tid csh
|
||||||
(Just (_,_, True)) -> pure . mr $ MsgSubmissionAlreadyExistsFor email
|
(Just (_,_, True)) -> pure . mr $ MsgSubmissionAlreadyExistsFor email
|
||||||
_other -> mempty
|
_other -> mempty
|
||||||
, case length participants `compare` maxParticipants of
|
, case fromIntegral (length participants) `compare` maxParticipants of
|
||||||
LT -> mempty
|
LT -> mempty
|
||||||
_ -> pure $ mr MsgTooManyParticipants
|
_ -> pure $ mr MsgTooManyParticipants
|
||||||
]
|
]
|
||||||
|
|||||||
@ -11,6 +11,7 @@ module Handler.Utils.DateTime
|
|||||||
, formatTime, formatTime', formatTimeW
|
, formatTime, formatTime', formatTimeW
|
||||||
, getTimeLocale, getDateTimeFormat
|
, getTimeLocale, getDateTimeFormat
|
||||||
, validDateTimeFormats, dateTimeFormatOptions
|
, validDateTimeFormats, dateTimeFormatOptions
|
||||||
|
, formatTimeMail
|
||||||
, addOneWeek
|
, addOneWeek
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@ -26,6 +27,8 @@ 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
|
||||||
|
|
||||||
@ -58,6 +61,9 @@ formatTime proj t = flip formatTime' t =<< (unDateTimeFormat <$> getDateTimeForm
|
|||||||
formatTimeW :: (HasLocalTime t) => SelDateTimeFormat -> t -> Widget
|
formatTimeW :: (HasLocalTime t) => SelDateTimeFormat -> t -> Widget
|
||||||
formatTimeW s t = toWidget =<< formatTime s t
|
formatTimeW s t = toWidget =<< formatTime s t
|
||||||
|
|
||||||
|
formatTimeMail :: (MonadMail m, HasLocalTime t) => SelDateTimeFormat -> t -> m Text
|
||||||
|
formatTimeMail sel t = fmap fromString $ Time.formatTime <$> (getTimeLocale' . mailLanguages <$> askMailLanguages) <*> (unDateTimeFormat <$> askMailDateTimeFormat sel) <*> pure (toLocalTime t)
|
||||||
|
|
||||||
getTimeLocale :: (MonadHandler m, HandlerSite m ~ UniWorX) => m TimeLocale
|
getTimeLocale :: (MonadHandler m, HandlerSite m ~ UniWorX) => m TimeLocale
|
||||||
getTimeLocale = getTimeLocale' <$> languages
|
getTimeLocale = getTimeLocale' <$> languages
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,10 @@ import Data.Scientific (Scientific)
|
|||||||
import Data.Ratio
|
import Data.Ratio
|
||||||
import Text.Read (readMaybe)
|
import Text.Read (readMaybe)
|
||||||
|
|
||||||
|
import Data.Maybe (fromJust)
|
||||||
|
|
||||||
|
import Utils.Lens
|
||||||
|
|
||||||
----------------------------
|
----------------------------
|
||||||
-- Buttons (new version ) --
|
-- Buttons (new version ) --
|
||||||
----------------------------
|
----------------------------
|
||||||
@ -310,23 +314,126 @@ multiFileField permittedFiles' = Field{..}
|
|||||||
Right _ -> return ()
|
Right _ -> return ()
|
||||||
Left r -> yield r
|
Left r -> yield r
|
||||||
|
|
||||||
sheetTypeAFormReq :: FieldSettings UniWorX -> Maybe SheetType -> AForm Handler SheetType
|
data SheetType' = Bonus' | Normal' | Pass' | NotGraded'
|
||||||
sheetTypeAFormReq d Nothing =
|
deriving (Eq, Ord, Read, Show, Enum, Bounded)
|
||||||
-- TODO, offer options to choose between Normal/Bonus/Pass
|
|
||||||
(Normal . toPoints) <$> areq (natField "Punkte") d Nothing
|
|
||||||
sheetTypeAFormReq d (Just (Normal p)) =
|
|
||||||
-- TODO, offer options to choose between Normal/Bonus/Pass
|
|
||||||
(Normal . toPoints) <$> areq (natField "Punkte") d (Just $ fromPoints p)
|
|
||||||
sheetTypeAFormReq d (Just (NotGraded)) = pure NotGraded
|
|
||||||
|
|
||||||
|
instance Universe SheetType'
|
||||||
|
instance Finite SheetType'
|
||||||
|
|
||||||
|
$(return [])
|
||||||
|
|
||||||
|
instance PathPiece SheetType' where
|
||||||
|
toPathPiece = $(nullaryToPathPiece ''SheetType' [intercalate "-" . splitCamel , fromJust . stripSuffix "'"])
|
||||||
|
fromPathPiece = finiteFromPathPiece
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX SheetType' where
|
||||||
|
renderMessage f ls = \case
|
||||||
|
Bonus' -> render MsgSheetTypeBonus
|
||||||
|
Normal' -> render MsgSheetTypeNormal
|
||||||
|
Pass' -> render MsgSheetTypePass
|
||||||
|
NotGraded' -> render MsgSheetTypeNotGraded
|
||||||
|
where
|
||||||
|
render = renderMessage f ls
|
||||||
|
|
||||||
|
data SheetGroup' = Arbitrary' | RegisteredGroups' | NoGroups'
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded)
|
||||||
|
|
||||||
|
instance Universe SheetGroup'
|
||||||
|
instance Finite SheetGroup'
|
||||||
|
|
||||||
|
$(return [])
|
||||||
|
|
||||||
|
instance PathPiece SheetGroup' where
|
||||||
|
toPathPiece = $(nullaryToPathPiece ''SheetGroup' [intercalate "-" . splitCamel , fromJust . stripSuffix "'"])
|
||||||
|
fromPathPiece = finiteFromPathPiece
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX SheetGroup' where
|
||||||
|
renderMessage f ls = \case
|
||||||
|
Arbitrary' -> render MsgSheetGroupArbitrary
|
||||||
|
RegisteredGroups' -> render MsgSheetGroupRegisteredGroups
|
||||||
|
NoGroups' -> render MsgSheetGroupNoGroups
|
||||||
|
where
|
||||||
|
render = renderMessage f ls
|
||||||
|
|
||||||
|
sheetTypeAFormReq :: FieldSettings UniWorX -> Maybe SheetType -> AForm Handler SheetType
|
||||||
|
sheetTypeAFormReq FieldSettings{..} template = formToAForm $ do
|
||||||
|
let
|
||||||
|
selOptions = Map.fromList
|
||||||
|
[ ( Bonus', renderAForm' $ Bonus <$> maxPointsReq )
|
||||||
|
, ( Normal', renderAForm' $ Normal <$> maxPointsReq )
|
||||||
|
, ( Pass', renderAForm' $ Pass
|
||||||
|
<$> maxPointsReq
|
||||||
|
<*> areq pointsField (fslpI MsgSheetTypePassingPoints "Punkte" & noValidate) (preview _passingPoints =<< template)
|
||||||
|
)
|
||||||
|
, ( NotGraded', return (FormSuccess NotGraded, Nothing) )
|
||||||
|
]
|
||||||
|
(res, selView) <- multiAction selOptions (classify' <$> template)
|
||||||
|
|
||||||
|
fvId <- maybe newIdent return fsId
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
|
return (res,
|
||||||
|
[ FieldView
|
||||||
|
{ fvLabel = toHtml $ mr fsLabel
|
||||||
|
, fvTooltip = toHtml . mr <$> fsTooltip
|
||||||
|
, fvId
|
||||||
|
, fvInput = selView
|
||||||
|
, fvErrors = case res of
|
||||||
|
FormFailure [e] -> Just $ toHtml e
|
||||||
|
_ -> Nothing
|
||||||
|
, fvRequired = True
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
where
|
||||||
|
renderAForm' = fmap (over _2 Just) . ($ mempty) . renderAForm FormStandard
|
||||||
|
|
||||||
|
maxPointsReq = areq pointsField (fslpI MsgSheetTypeMaxPoints "Punkte" & noValidate) (preview _maxPoints =<< template)
|
||||||
|
|
||||||
|
classify' :: SheetType -> SheetType'
|
||||||
|
classify' = \case
|
||||||
|
Bonus _ -> Bonus'
|
||||||
|
Normal _ -> Normal'
|
||||||
|
Pass _ _ -> Pass'
|
||||||
|
NotGraded -> NotGraded'
|
||||||
|
|
||||||
sheetGroupAFormReq :: FieldSettings UniWorX -> Maybe SheetGroup -> AForm Handler SheetGroup
|
sheetGroupAFormReq :: FieldSettings UniWorX -> Maybe SheetGroup -> AForm Handler SheetGroup
|
||||||
sheetGroupAFormReq d (Just (Arbitrary n)) | n >= 1 =
|
sheetGroupAFormReq FieldSettings{..} template = formToAForm $ do
|
||||||
-- TODO, offer options to choose between Arbitrary/Registered/NoGroups
|
let
|
||||||
Arbitrary <$> areq (natField "Abgabegruppengröße") d (Just n)
|
selOptions = Map.fromList
|
||||||
sheetGroupAFormReq d _other = -- TODO
|
[ ( Arbitrary', renderAForm' $ Arbitrary
|
||||||
-- TODO, offer options to choose between Arbitrary/Registered/NoGroups
|
<$> areq (natField "Gruppengröße") (fslI MsgSheetGroupMaxGroupsize & noValidate) (preview _maxParticipants =<< template)
|
||||||
Arbitrary <$> areq (natField "Abgabegruppengröße") d (Just 1)
|
)
|
||||||
|
, ( RegisteredGroups', return (FormSuccess RegisteredGroups, Nothing) )
|
||||||
|
, ( NoGroups', return (FormSuccess NoGroups, Nothing) )
|
||||||
|
]
|
||||||
|
(res, selView) <- multiAction selOptions (classify' <$> template)
|
||||||
|
|
||||||
|
fvId <- maybe newIdent return fsId
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
|
return (res,
|
||||||
|
[ FieldView
|
||||||
|
{ fvLabel = toHtml $ mr fsLabel
|
||||||
|
, fvTooltip = toHtml . mr <$> fsTooltip
|
||||||
|
, fvId
|
||||||
|
, fvInput = selView
|
||||||
|
, fvErrors = case res of
|
||||||
|
FormFailure [e] -> Just $ toHtml e
|
||||||
|
_ -> Nothing
|
||||||
|
, fvRequired = True
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
where
|
||||||
|
renderAForm' = fmap (over _2 Just) . ($ mempty) . renderAForm FormStandard
|
||||||
|
|
||||||
|
classify' :: SheetGroup -> SheetGroup'
|
||||||
|
classify' = \case
|
||||||
|
Arbitrary _ -> Arbitrary'
|
||||||
|
RegisteredGroups -> RegisteredGroups'
|
||||||
|
NoGroups -> NoGroups'
|
||||||
|
|
||||||
|
|
||||||
{-
|
{-
|
||||||
dayTimeField :: FieldSettings UniWorX -> Maybe UTCTime -> Form Handler UTCTime
|
dayTimeField :: FieldSettings UniWorX -> Maybe UTCTime -> Form Handler UTCTime
|
||||||
@ -440,12 +547,13 @@ aforced field settings val = formToAForm $ second pure <$> mforced field setting
|
|||||||
|
|
||||||
multiAction :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action)
|
multiAction :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action)
|
||||||
=> Map action (MForm (HandlerT UniWorX IO) (FormResult a, Maybe Widget))
|
=> Map action (MForm (HandlerT UniWorX IO) (FormResult a, Maybe Widget))
|
||||||
|
-> Maybe action
|
||||||
-> MForm (HandlerT UniWorX IO) (FormResult a, Widget)
|
-> MForm (HandlerT UniWorX IO) (FormResult a, Widget)
|
||||||
multiAction acts = do
|
multiAction acts defAction = do
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
let
|
let
|
||||||
options = OptionList [ Option (mr a) a (toPathPiece a) | a <- Map.keys acts ] fromPathPiece
|
options = OptionList [ Option (mr a) a (toPathPiece a) | a <- Map.keys acts ] fromPathPiece
|
||||||
(actionRes, actionView) <- mreq (selectField $ return options) "" Nothing
|
(actionRes, actionView) <- mreq (selectField $ return options) "" defAction
|
||||||
results <- sequence acts
|
results <- sequence acts
|
||||||
let actionWidgets = Map.foldrWithKey (\act -> \case (_, Just w) -> ($(widgetFile "widgets/multiAction") :); (_, Nothing) -> id) [] results
|
let actionWidgets = Map.foldrWithKey (\act -> \case (_, Just w) -> ($(widgetFile "widgets/multiAction") :); (_, Nothing) -> id) [] results
|
||||||
actionResults = Map.map fst results
|
actionResults = Map.map fst results
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
, TypeFamilies
|
, TypeFamilies
|
||||||
, FlexibleContexts
|
, FlexibleContexts
|
||||||
, ViewPatterns
|
, ViewPatterns
|
||||||
|
, LambdaCase
|
||||||
#-}
|
#-}
|
||||||
|
|
||||||
module Handler.Utils.Mail
|
module Handler.Utils.Mail
|
||||||
@ -44,9 +45,24 @@ userMailT :: ( MonadHandler m
|
|||||||
, MonadLogger m
|
, MonadLogger m
|
||||||
) => UserId -> MailT m a -> m a
|
) => UserId -> MailT m a -> m a
|
||||||
userMailT uid mAct = do
|
userMailT uid mAct = do
|
||||||
User{userEmail, userDisplayName, userMailLanguages} <- liftHandlerT . runDB $ getJust uid
|
User
|
||||||
let addr = Address (Just userDisplayName) $ CI.original userEmail
|
{ userEmail
|
||||||
mailT userMailLanguages $ do
|
, userDisplayName
|
||||||
|
, userMailLanguages
|
||||||
|
, userDateTimeFormat
|
||||||
|
, userDateFormat
|
||||||
|
, userTimeFormat
|
||||||
|
} <- liftHandlerT . runDB $ getJust uid
|
||||||
|
let
|
||||||
|
addr = Address (Just userDisplayName) $ CI.original userEmail
|
||||||
|
ctx = MailContext
|
||||||
|
{ mcLanguages = userMailLanguages
|
||||||
|
, mcDateTimeFormat = \case
|
||||||
|
SelFormatDateTime -> userDateTimeFormat
|
||||||
|
SelFormatDate -> userDateFormat
|
||||||
|
SelFormatTime -> userTimeFormat
|
||||||
|
}
|
||||||
|
mailT ctx $ do
|
||||||
_mailTo .= pure addr
|
_mailTo .= pure addr
|
||||||
mAct
|
mAct
|
||||||
|
|
||||||
|
|||||||
37
src/Jobs.hs
37
src/Jobs.hs
@ -20,6 +20,7 @@ module Jobs
|
|||||||
|
|
||||||
import Import hiding ((.=))
|
import Import hiding ((.=))
|
||||||
import Handler.Utils.Mail
|
import Handler.Utils.Mail
|
||||||
|
import Handler.Utils.DateTime
|
||||||
|
|
||||||
import Jobs.Types
|
import Jobs.Types
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ import qualified Database.Esqueleto as E
|
|||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
import Text.Shakespeare.Text
|
||||||
import Text.Hamlet
|
import Text.Hamlet
|
||||||
|
|
||||||
|
|
||||||
@ -161,24 +163,32 @@ performJob JobQueueNotification{ jNotification = n@NotificationSubmissionRated{.
|
|||||||
forM recipients $ queueJobUnsafe . flip JobSendNotification n
|
forM recipients $ queueJobUnsafe . flip JobSendNotification n
|
||||||
forM_ jIds $ writeJobCtl . JobCtlPerform
|
forM_ jIds $ writeJobCtl . JobCtlPerform
|
||||||
performJob JobSendNotification{ jNotification = NotificationSubmissionRated{..}, jRecipient } = userMailT jRecipient $ do
|
performJob JobSendNotification{ jNotification = NotificationSubmissionRated{..}, jRecipient } = userMailT jRecipient $ do
|
||||||
(Course{..}, Sheet{..}, Submission{..}) <- liftHandlerT . runDB $ do
|
(Course{..}, Sheet{..}, Submission{..}, corrector) <- liftHandlerT . runDB $ do
|
||||||
submission <- getJust nSubmission
|
submission@Submission{submissionRatingBy} <- getJust nSubmission
|
||||||
sheet <- belongsToJust submissionSheet submission
|
sheet <- belongsToJust submissionSheet submission
|
||||||
course <- belongsToJust sheetCourse sheet
|
course <- belongsToJust sheetCourse sheet
|
||||||
return (course, sheet, submission)
|
corrector <- traverse getJust submissionRatingBy
|
||||||
csId <- encrypt nSubmission
|
return (course, sheet, submission, corrector)
|
||||||
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
|
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
|
||||||
|
|
||||||
|
csid <- encrypt nSubmission
|
||||||
MsgRenderer mr <- getMailMsgRenderer
|
MsgRenderer mr <- getMailMsgRenderer
|
||||||
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
|
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
|
||||||
|
submissionRatingTime' <- traverse (formatTimeMail SelFormatDateTime) submissionRatingTime
|
||||||
|
let tid = courseTerm
|
||||||
|
ssh = courseSchool
|
||||||
|
csh = courseShorthand
|
||||||
|
shn = sheetName
|
||||||
|
|
||||||
-- TODO: provide convienience template-haskell for `addAlternatives`
|
-- TODO: provide convienience template-haskell for `addAlternatives`
|
||||||
addAlternatives $ do
|
addAlternatives $ do
|
||||||
provideAlternative $ Aeson.object
|
provideAlternative $ Aeson.object
|
||||||
[ "submission" Aeson..= (ciphertext csId :: UUID)
|
[ "submission" Aeson..= ciphertext csid
|
||||||
, "submission-rating-points" Aeson..= submissionRatingPoints
|
, "submission-rating-points" Aeson..= submissionRatingPoints
|
||||||
, "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-passed" Aeson..= ((>=) <$> submissionRatingPoints <*> preview _passingPoints sheetType)
|
||||||
, "sheet-name" Aeson..= sheetName
|
, "sheet-name" Aeson..= sheetName
|
||||||
, "sheet-type" Aeson..= sheetType
|
, "sheet-type" Aeson..= sheetType
|
||||||
, "course-name" Aeson..= courseName
|
, "course-name" Aeson..= courseName
|
||||||
@ -186,9 +196,20 @@ performJob JobSendNotification{ jNotification = NotificationSubmissionRated{..},
|
|||||||
, "course-term" Aeson..= courseTerm
|
, "course-term" Aeson..= courseTerm
|
||||||
, "course-school" Aeson..= courseSchool
|
, "course-school" Aeson..= courseSchool
|
||||||
]
|
]
|
||||||
provideAlternative $ \(MsgRenderer mr) -> ($(textFile "templates/mail/submissionRated.txt") :: TextUrl (Route UniWorX))
|
-- provideAlternative $ \(MsgRenderer mr) -> ($(textFile "templates/mail/submissionRated.txt") :: TextUrl (Route UniWorX)) -- textFile does not support control statements
|
||||||
providePreferredAlternative $ \(MsgRenderer mr) -> ($(ihamletFile "templates/mail/submissionRated.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
providePreferredAlternative $ \(MsgRenderer mr) -> ($(ihamletFile "templates/mail/submissionRated.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
||||||
performJob JobSendTestEmail{..} = mailT jLanguages $ do
|
performJob JobSendTestEmail{..} = mailT jMailContext $ do
|
||||||
_mailTo .= [Address Nothing jEmail]
|
_mailTo .= [Address Nothing jEmail]
|
||||||
setSubjectI MsgMailTestSubject
|
setSubjectI MsgMailTestSubject
|
||||||
addPart $ \(MsgRenderer mr) -> mr MsgMailTestContent
|
now <- liftIO getCurrentTime
|
||||||
|
nDT <- formatTimeMail SelFormatDateTime now
|
||||||
|
nD <- formatTimeMail SelFormatDate now
|
||||||
|
nT <- formatTimeMail SelFormatTime now
|
||||||
|
addPart $ \(MsgRenderer mr) -> ([text|
|
||||||
|
#{mr MsgMailTestContent}
|
||||||
|
|
||||||
|
#{mr MsgMailTestDateTime}
|
||||||
|
* #{nDT}
|
||||||
|
* #{nD}
|
||||||
|
* #{nT}
|
||||||
|
|] :: TextUrl (Route UniWorX))
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import Data.Aeson.TH (deriveJSON)
|
|||||||
|
|
||||||
|
|
||||||
data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notification }
|
data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notification }
|
||||||
| JobSendTestEmail { jEmail :: Text, jLanguages :: MailLanguages }
|
| JobSendTestEmail { jEmail :: Text, jMailContext :: MailContext }
|
||||||
| JobQueueNotification { jNotification :: Notification }
|
| JobQueueNotification { jNotification :: Notification }
|
||||||
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
||||||
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
||||||
|
|||||||
34
src/Mail.hs
34
src/Mail.hs
@ -16,6 +16,7 @@
|
|||||||
, QuasiQuotes
|
, QuasiQuotes
|
||||||
, RankNTypes
|
, RankNTypes
|
||||||
, ScopedTypeVariables
|
, ScopedTypeVariables
|
||||||
|
, DeriveDataTypeable
|
||||||
#-}
|
#-}
|
||||||
|
|
||||||
module Mail
|
module Mail
|
||||||
@ -23,7 +24,7 @@ module Mail
|
|||||||
module Network.Mail.Mime
|
module Network.Mail.Mime
|
||||||
-- * MailT
|
-- * MailT
|
||||||
, MailT, defMailT
|
, MailT, defMailT
|
||||||
, MailSmtpData(..), MailLanguages(..)
|
, MailSmtpData(..), MailContext(..), MailLanguages(..)
|
||||||
, MonadMail(..)
|
, MonadMail(..)
|
||||||
, getMailMessageRender, getMailMsgRenderer
|
, getMailMessageRender, getMailMsgRenderer
|
||||||
-- * YesodMail
|
-- * YesodMail
|
||||||
@ -66,6 +67,8 @@ 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
|
||||||
|
|
||||||
@ -112,16 +115,20 @@ import Data.Aeson (Options(..))
|
|||||||
import Data.Aeson.TH
|
import Data.Aeson.TH
|
||||||
import Utils (MsgRendererS(..))
|
import Utils (MsgRendererS(..))
|
||||||
import Utils.PathPiece (splitCamel)
|
import Utils.PathPiece (splitCamel)
|
||||||
|
import Utils.DateTime
|
||||||
|
|
||||||
|
import Data.Universe.Instances.Reverse ()
|
||||||
|
import Data.Universe.Instances.Reverse.JSON ()
|
||||||
|
import Data.Universe.Instances.Reverse.Hashable ()
|
||||||
|
|
||||||
makeLenses_ ''Mail
|
makeLenses_ ''Mail
|
||||||
makeLenses_ ''Part
|
makeLenses_ ''Part
|
||||||
|
|
||||||
|
|
||||||
newtype MailT m a = MailT { unMailT :: RWST MailLanguages 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 MailLanguages
|
, MonadState Mail, MonadWriter MailSmtpData, MonadReader MailContext
|
||||||
)
|
)
|
||||||
|
|
||||||
instance {-# OVERLAPPING #-} (MonadCrypto m, MonadCryptoKey m ~ CryptoIDKey) => MonadCrypto (MailT m) where
|
instance {-# OVERLAPPING #-} (MonadCrypto m, MonadCryptoKey m ~ CryptoIDKey) => MonadCrypto (MailT m) where
|
||||||
@ -146,12 +153,27 @@ instance Default MailLanguages where
|
|||||||
|
|
||||||
instance Hashable MailLanguages
|
instance Hashable MailLanguages
|
||||||
|
|
||||||
|
data MailContext = MailContext
|
||||||
|
{ mcLanguages :: MailLanguages
|
||||||
|
, mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat
|
||||||
|
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
|
||||||
|
} ''MailContext
|
||||||
|
|
||||||
|
instance Hashable MailContext
|
||||||
|
|
||||||
|
makeLenses_ ''MailContext
|
||||||
|
|
||||||
class (MonadHandler m, MonadState Mail m) => MonadMail m where
|
class (MonadHandler m, MonadState Mail m) => MonadMail m where
|
||||||
askMailLanguages :: m MailLanguages
|
askMailLanguages :: m MailLanguages
|
||||||
|
askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat
|
||||||
tellMailSmtpData :: MailSmtpData -> m ()
|
tellMailSmtpData :: MailSmtpData -> m ()
|
||||||
|
|
||||||
instance MonadHandler m => MonadMail (MailT m) where
|
instance MonadHandler m => MonadMail (MailT m) where
|
||||||
askMailLanguages = ask
|
askMailLanguages = view _mcLanguages
|
||||||
|
askMailDateTimeFormat = (view _mcDateTimeFormat ??)
|
||||||
tellMailSmtpData = tell
|
tellMailSmtpData = tell
|
||||||
|
|
||||||
data VerpMode = VerpNone
|
data VerpMode = VerpNone
|
||||||
@ -212,7 +234,7 @@ class Yesod site => YesodMail site where
|
|||||||
, HandlerSite m ~ site
|
, HandlerSite m ~ site
|
||||||
, MonadBaseControl IO m
|
, MonadBaseControl IO m
|
||||||
, MonadLogger m
|
, MonadLogger m
|
||||||
) => MailLanguages -> MailT m a -> m a
|
) => MailContext -> MailT m a -> m a
|
||||||
mailT = defMailT
|
mailT = defMailT
|
||||||
|
|
||||||
defaultMailLayout :: ( MonadHandler m
|
defaultMailLayout :: ( MonadHandler m
|
||||||
@ -238,7 +260,7 @@ defMailT :: ( MonadHandler m
|
|||||||
, YesodMail (HandlerSite m)
|
, YesodMail (HandlerSite m)
|
||||||
, MonadBaseControl IO m
|
, MonadBaseControl IO m
|
||||||
, MonadLogger m
|
, MonadLogger m
|
||||||
) => MailLanguages -- ^ Languages in priority order
|
) => MailContext
|
||||||
-> MailT m a
|
-> MailT m a
|
||||||
-> m a
|
-> m a
|
||||||
defMailT ls (MailT mail) = do
|
defMailT ls (MailT mail) = do
|
||||||
|
|||||||
@ -13,12 +13,15 @@
|
|||||||
|
|
||||||
module Model.Types
|
module Model.Types
|
||||||
( module Model.Types
|
( module Model.Types
|
||||||
|
, module Numeric.Natural
|
||||||
, module Mail
|
, module Mail
|
||||||
|
, module Utils.DateTime
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude
|
import ClassyPrelude
|
||||||
import Utils
|
import Utils
|
||||||
import Control.Lens
|
import Control.Lens
|
||||||
|
import Utils.Lens.TH
|
||||||
|
|
||||||
import Data.Set (Set)
|
import Data.Set (Set)
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
@ -68,6 +71,9 @@ import Data.Universe.Instances.Reverse ()
|
|||||||
import qualified Yesod.Auth.Util.PasswordStore as PWStore
|
import qualified Yesod.Auth.Util.PasswordStore as PWStore
|
||||||
|
|
||||||
import Mail (MailLanguages(..))
|
import Mail (MailLanguages(..))
|
||||||
|
import Utils.DateTime (DateTimeFormat(..), SelDateTimeFormat(..))
|
||||||
|
|
||||||
|
import Numeric.Natural
|
||||||
|
|
||||||
|
|
||||||
instance PathPiece UUID where
|
instance PathPiece UUID where
|
||||||
@ -123,6 +129,8 @@ instance DisplayAble SheetType where
|
|||||||
deriveJSON defaultOptions ''SheetType
|
deriveJSON defaultOptions ''SheetType
|
||||||
derivePersistFieldJSON ''SheetType
|
derivePersistFieldJSON ''SheetType
|
||||||
|
|
||||||
|
makeLenses_ ''SheetType
|
||||||
|
|
||||||
data SheetTypeSummary = SheetTypeSummary
|
data SheetTypeSummary = SheetTypeSummary
|
||||||
{ sumBonusPoints :: Sum Points
|
{ sumBonusPoints :: Sum Points
|
||||||
, sumNormalPoints :: Sum Points
|
, sumNormalPoints :: Sum Points
|
||||||
@ -145,13 +153,15 @@ sheetTypeSum (NotGraded, _ ) = mempty { numNotGraded = Sum 1 }
|
|||||||
|
|
||||||
|
|
||||||
data SheetGroup
|
data SheetGroup
|
||||||
= Arbitrary { maxParticipants :: Int }
|
= Arbitrary { maxParticipants :: Natural }
|
||||||
| RegisteredGroups
|
| RegisteredGroups
|
||||||
| NoGroups
|
| NoGroups
|
||||||
deriving (Show, Read, Eq)
|
deriving (Show, Read, Eq)
|
||||||
deriveJSON defaultOptions ''SheetGroup
|
deriveJSON defaultOptions ''SheetGroup
|
||||||
derivePersistFieldJSON ''SheetGroup
|
derivePersistFieldJSON ''SheetGroup
|
||||||
|
|
||||||
|
makeLenses_ ''SheetGroup
|
||||||
|
|
||||||
data SheetFileType = SheetExercise | SheetHint | SheetSolution | SheetMarking
|
data SheetFileType = SheetExercise | SheetHint | SheetSolution | SheetMarking
|
||||||
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
deriving (Show, Read, Eq, Ord, Enum, Bounded)
|
||||||
derivePersistField "SheetFileType"
|
derivePersistField "SheetFileType"
|
||||||
@ -416,12 +426,6 @@ instance PathPiece obj => PathPiece (ZIPArchiveName obj) where
|
|||||||
fromPathPiece = fmap ZIPArchiveName . fromPathPiece <=< (stripSuffix `on` CI.foldCase) ".zip"
|
fromPathPiece = fmap ZIPArchiveName . fromPathPiece <=< (stripSuffix `on` CI.foldCase) ".zip"
|
||||||
toPathPiece = (<> ".zip") . toPathPiece . unZIPArchiveName
|
toPathPiece = (<> ".zip") . toPathPiece . unZIPArchiveName
|
||||||
|
|
||||||
newtype DateTimeFormat = DateTimeFormat { unDateTimeFormat :: String }
|
|
||||||
deriving (Eq, Ord, Read, Show, ToJSON, FromJSON, PersistField, PersistFieldSql, IsString)
|
|
||||||
|
|
||||||
data SelDateTimeFormat = SelFormatDateTime | SelFormatDate | SelFormatTime
|
|
||||||
deriving (Eq, Ord, Read, Show, Enum, Bounded)
|
|
||||||
|
|
||||||
|
|
||||||
data CorrectorState = CorrectorNormal | CorrectorMissing | CorrectorExcused
|
data CorrectorState = CorrectorNormal | CorrectorMissing | CorrectorExcused
|
||||||
deriving (Eq, Ord, Read, Show, Enum, Bounded)
|
deriving (Eq, Ord, Read, Show, Enum, Bounded)
|
||||||
|
|||||||
@ -2,7 +2,12 @@
|
|||||||
, TemplateHaskell
|
, TemplateHaskell
|
||||||
, QuasiQuotes
|
, QuasiQuotes
|
||||||
, StandaloneDeriving
|
, StandaloneDeriving
|
||||||
|
, DerivingStrategies
|
||||||
, DeriveLift
|
, DeriveLift
|
||||||
|
, DeriveDataTypeable
|
||||||
|
, DeriveGeneric
|
||||||
|
, GeneralizedNewtypeDeriving
|
||||||
|
, OverloadedStrings
|
||||||
#-}
|
#-}
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
@ -10,6 +15,8 @@ module Utils.DateTime
|
|||||||
( timeLocaleMap
|
( timeLocaleMap
|
||||||
, TimeLocale(..)
|
, TimeLocale(..)
|
||||||
, currentYear
|
, currentYear
|
||||||
|
, DateTimeFormat(..)
|
||||||
|
, SelDateTimeFormat(..)
|
||||||
, module Data.Time.Zones
|
, module Data.Time.Zones
|
||||||
, module Data.Time.Zones.TH
|
, module Data.Time.Zones.TH
|
||||||
) where
|
) where
|
||||||
@ -25,6 +32,18 @@ import Language.Haskell.TH
|
|||||||
import Language.Haskell.TH.Syntax (Lift(..))
|
import Language.Haskell.TH.Syntax (Lift(..))
|
||||||
import Instances.TH.Lift ()
|
import Instances.TH.Lift ()
|
||||||
|
|
||||||
|
import Data.Data (Data)
|
||||||
|
import Data.Typeable (Typeable)
|
||||||
|
import Data.Universe
|
||||||
|
|
||||||
|
import Database.Persist.Sql (PersistField, PersistFieldSql)
|
||||||
|
|
||||||
|
import Data.Aeson.Types (toJSONKeyText)
|
||||||
|
import Data.Aeson
|
||||||
|
import Data.Aeson.TH
|
||||||
|
|
||||||
|
import Utils.PathPiece
|
||||||
|
|
||||||
deriving instance Lift TimeZone
|
deriving instance Lift TimeZone
|
||||||
deriving instance Lift TimeLocale
|
deriving instance Lift TimeLocale
|
||||||
|
|
||||||
@ -63,3 +82,26 @@ currentYear = do
|
|||||||
now <- runIO getCurrentTime
|
now <- runIO getCurrentTime
|
||||||
let (year, _, _) = toGregorian $ utctDay now
|
let (year, _, _) = toGregorian $ utctDay now
|
||||||
[e|year|]
|
[e|year|]
|
||||||
|
|
||||||
|
|
||||||
|
newtype DateTimeFormat = DateTimeFormat { unDateTimeFormat :: String }
|
||||||
|
deriving (Eq, Ord, Read, Show, Data, Generic, Typeable)
|
||||||
|
deriving newtype (ToJSON, FromJSON, PersistField, PersistFieldSql, IsString)
|
||||||
|
|
||||||
|
instance Hashable DateTimeFormat
|
||||||
|
|
||||||
|
data SelDateTimeFormat = SelFormatDateTime | SelFormatDate | SelFormatTime
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Data, Generic, Typeable)
|
||||||
|
|
||||||
|
instance Universe SelDateTimeFormat
|
||||||
|
instance Finite SelDateTimeFormat
|
||||||
|
instance Hashable SelDateTimeFormat
|
||||||
|
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ constructorTagModifier = intercalate "-" . map toLower . drop 2 . splitCamel
|
||||||
|
} ''SelDateTimeFormat
|
||||||
|
|
||||||
|
instance ToJSONKey SelDateTimeFormat where
|
||||||
|
toJSONKey = toJSONKeyText $ \v -> let String txt = toJSON v in txt
|
||||||
|
instance FromJSONKey SelDateTimeFormat where
|
||||||
|
fromJSONKey = FromJSONKeyTextParser $ parseJSON . String
|
||||||
|
|||||||
@ -143,6 +143,9 @@ addDatalist field mValues = field
|
|||||||
|]
|
|]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
noValidate :: FieldSettings site -> FieldSettings site
|
||||||
|
noValidate = addAttr "formnovalidate" ""
|
||||||
|
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
-- Unique Form Identifiers to avoid accidents --
|
-- Unique Form Identifiers to avoid accidents --
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
@ -239,7 +242,7 @@ reorderField optList = Field{..}
|
|||||||
OptionList{..} <- liftHandlerT optList
|
OptionList{..} <- liftHandlerT optList
|
||||||
let
|
let
|
||||||
olNum = fromIntegral $ length olOptions
|
olNum = fromIntegral $ length olOptions
|
||||||
selOptions = traceShowId . 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
|
||||||
|
|||||||
@ -35,5 +35,4 @@
|
|||||||
^{modal "Klick mich für Ajax-Test" (Left UsersR)}
|
^{modal "Klick mich für Ajax-Test" (Left UsersR)}
|
||||||
^{modal "Klick mich für Content-Test" (Right "Test Inhalt für Modal")}
|
^{modal "Klick mich für Content-Test" (Right "Test Inhalt für Modal")}
|
||||||
<li>
|
<li>
|
||||||
<form method=post action=@{AdminTestR} enctype=#{emailEnctype}>
|
^{modal "Email-Test" (Right emailWidget')}
|
||||||
^{emailWidget}
|
|
||||||
|
|||||||
3
templates/correction-user.cassius
Normal file
3
templates/correction-user.cassius
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.comment
|
||||||
|
white-space: pre-wrap
|
||||||
|
font-family: monospace
|
||||||
@ -36,4 +36,4 @@
|
|||||||
$maybe comment <- ratingComment
|
$maybe comment <- ratingComment
|
||||||
<tr .table__row>
|
<tr .table__row>
|
||||||
<th .table__th>_{MsgRatingComment}
|
<th .table__th>_{MsgRatingComment}
|
||||||
<td .table__td style="white-space: pre;">#{comment}
|
<td .table__td .comment>#{comment}
|
||||||
|
|||||||
@ -1,4 +1,62 @@
|
|||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
<body>
|
<body>
|
||||||
<h1>
|
<h1>
|
||||||
_{MsgMailSubmissionRatedIntro (CI.original courseName) termDesc}
|
_{MsgMailSubmissionRatedIntro (CI.original courseName) termDesc}
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
_{MsgSubmission}
|
||||||
|
<dd>
|
||||||
|
<a href=@{CSubmissionR tid ssh csh shn csid SubShowR}>
|
||||||
|
#{display csid}
|
||||||
|
$maybe User{..} <- corrector
|
||||||
|
<dt>
|
||||||
|
_{MsgRatingBy}
|
||||||
|
<dd>
|
||||||
|
#{display userDisplayName}
|
||||||
|
$maybe time <- submissionRatingTime'
|
||||||
|
<dt>
|
||||||
|
_{MsgRatingTime}
|
||||||
|
<dd>
|
||||||
|
#{time}
|
||||||
|
$maybe points <- submissionRatingPoints
|
||||||
|
$case sheetType
|
||||||
|
$of Bonus{..}
|
||||||
|
<dt>
|
||||||
|
_{MsgAchievedBonusPoints}
|
||||||
|
<dd>
|
||||||
|
_{MsgAchievedOf points maxPoints}
|
||||||
|
$of Normal{..}
|
||||||
|
<dt>
|
||||||
|
_{MsgAchievedNormalPoints}
|
||||||
|
<dd>
|
||||||
|
_{MsgAchievedOf points maxPoints}
|
||||||
|
$of Pass{..}
|
||||||
|
<dt>
|
||||||
|
_{MsgPassedResult}
|
||||||
|
<dd>
|
||||||
|
$if points >= passingPoints
|
||||||
|
_{MsgPassed}
|
||||||
|
$else
|
||||||
|
_{MsgNotPassed}
|
||||||
|
<dt>
|
||||||
|
_{MsgAchievedPassPoints}
|
||||||
|
<dd>
|
||||||
|
_{MsgPassAchievedOf points passingPoints maxPoints}
|
||||||
|
$of NotGraded
|
||||||
|
$maybe comment <- submissionRatingComment
|
||||||
|
<dt>
|
||||||
|
_{MsgRatingComment}
|
||||||
|
<dd .comment>
|
||||||
|
#{comment}
|
||||||
|
|||||||
@ -36,7 +36,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.utils.interactiveFieldset = function(form, fieldSets) {
|
window.utils.interactiveFieldset = function(form, fieldSets) {
|
||||||
|
|
||||||
var fields = fieldSets.map(function(fs) {
|
var fields = fieldSets.map(function(fs) {
|
||||||
return {
|
return {
|
||||||
fieldSet: fs,
|
fieldSet: fs,
|
||||||
|
|||||||
@ -1,10 +1,3 @@
|
|||||||
.hidden {
|
|
||||||
visibility: hidden;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
border: 0;
|
border: 0;
|
||||||
margin: 20px 0 30px;
|
margin: 20px 0 30px;
|
||||||
@ -13,3 +6,14 @@ fieldset {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group__input > fieldset {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user