Merge branch 'master' of gitlab.cip.ifi.lmu.de:jost/UniWorX into feat/exercises

This commit is contained in:
Gregor Kleen 2018-07-18 08:51:30 +02:00
commit 584d711cbc
38 changed files with 522 additions and 268 deletions

View File

@ -1,3 +1,6 @@
* Version 10.07.2018
Bugfixes, wählbares Format für Datum
* Version 04.07.2018 * Version 04.07.2018
Hinweis eingefügt, dass alle Daten des Systems spätestens im Dezember 2018 Hinweis eingefügt, dass alle Daten des Systems spätestens im Dezember 2018

View File

@ -40,7 +40,11 @@ ldap:
password: "_env:LDAPPW:" password: "_env:LDAPPW:"
basename: "_env:LDAPBN:" basename: "_env:LDAPBN:"
userDefaultFavourites: 12 default-favourites: 12
default-theme: Default
default-date-time-format: "%a %d %b %Y %R"
default-date-format: "%d.%m.%Y"
default-time-format: "%R"
cryptoid-keyfile: "_env:CRYPTOID_KEYFILE:cryptoid_key.bf" cryptoid-keyfile: "_env:CRYPTOID_KEYFILE:cryptoid_key.bf"

19
db.hs
View File

@ -5,6 +5,7 @@
{-# LANGUAGE PackageImports #-} {-# LANGUAGE PackageImports #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
import "uniworx" Import hiding (Option(..)) import "uniworx" Import hiding (Option(..))
import "uniworx" Application (db, getAppDevSettings) import "uniworx" Application (db, getAppDevSettings)
@ -47,7 +48,7 @@ main = do
fillDb :: DB () fillDb :: DB ()
fillDb = do fillDb = do
defaultFavourites <- getsYesod $ appDefaultFavourites . appSettings AppSettings{..} <- getsYesod appSettings
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let let
summer2017 = TermIdentifier 2017 Summer summer2017 = TermIdentifier 2017 Summer
@ -60,7 +61,10 @@ fillDb = do
, userEmail = "G.Kleen@campus.lmu.de" , userEmail = "G.Kleen@campus.lmu.de"
, userDisplayName = "Gregor Kleen" , userDisplayName = "Gregor Kleen"
, userMaxFavourites = 6 , userMaxFavourites = 6
, userTheme = AberdeenReds , userTheme = Default
, userDateTimeFormat = appDefaultDateTimeFormat
, userDateFormat = appDefaultDateFormat
, userTimeFormat = appDefaultTimeFormat
} }
fhamann <- insert User fhamann <- insert User
{ userPlugin = "LDAP" { userPlugin = "LDAP"
@ -68,8 +72,11 @@ fillDb = do
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "felix.hamann@campus.lmu.de" , userEmail = "felix.hamann@campus.lmu.de"
, userDisplayName = "Felix Hamann" , userDisplayName = "Felix Hamann"
, userMaxFavourites = defaultFavourites , userMaxFavourites = appDefaultMaxFavourites
, userTheme = Default , userTheme = Default
, userDateTimeFormat = appDefaultDateTimeFormat
, userDateFormat = appDefaultDateFormat
, userTimeFormat = appDefaultTimeFormat
} }
jost <- insert User jost <- insert User
{ userPlugin = "LDAP" { userPlugin = "LDAP"
@ -79,6 +86,9 @@ fillDb = do
, userDisplayName = "Steffen Jost" , userDisplayName = "Steffen Jost"
, userMaxFavourites = 14 , userMaxFavourites = 14
, userTheme = MossGreen , userTheme = MossGreen
, userDateTimeFormat = appDefaultDateTimeFormat
, userDateFormat = appDefaultDateFormat
, userTimeFormat = appDefaultTimeFormat
} }
void . insert $ User void . insert $ User
{ userPlugin = "LDAP" { userPlugin = "LDAP"
@ -88,6 +98,9 @@ fillDb = do
, userDisplayName = "Max Musterstudent" , userDisplayName = "Max Musterstudent"
, userMaxFavourites = 7 , userMaxFavourites = 7
, userTheme = AberdeenReds , userTheme = AberdeenReds
, userDateTimeFormat = appDefaultDateTimeFormat
, userDateFormat = appDefaultDateFormat
, userTimeFormat = appDefaultTimeFormat
} }
void . insert $ Term void . insert $ Term
{ termName = summer2017 { termName = summer2017

View File

@ -76,6 +76,7 @@ CorrectionHead tid@TermId courseShortHand@Text sheetName@Text cid@CryptoFileName
SubmissionMember g@Int: Mitabgebende(r) ##{display g} SubmissionMember g@Int: Mitabgebende(r) ##{display g}
SubmissionArchive: Zip-Archiv der Abgabedatei(en) SubmissionArchive: Zip-Archiv der Abgabedatei(en)
SubmissionFile: Datei zur Abgabe SubmissionFile: Datei zur Abgabe
SubmissionFiles: Abgegebene Dateien
SubmissionAlreadyExistsFor user@Text: #{user} hat bereits eine Abgabe zu diesem bÜbungsblatt. SubmissionAlreadyExistsFor user@Text: #{user} hat bereits eine Abgabe zu diesem bÜbungsblatt.
CorrectionsTitle: Zugewiesene Korrekturen CorrectionsTitle: Zugewiesene Korrekturen
@ -195,4 +196,14 @@ NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter
AdminFor: Administrator AdminFor: Administrator
LecturerFor: Dozent LecturerFor: Dozent
UserListTitle: Komprehensive Benutzerliste UserListTitle: Komprehensive Benutzerliste
DateTimeFormat: Datums- und Uhrzeitformat
DateFormat: Datumsformat
TimeFormat: Uhrzeitformat
InvalidDateTimeFormat: Ungültiges Datums- und Zeitformat, JJJJ-MM-TTTHH:MM[:SS] Format erwartet
AmbiguousUTCTime: Der angegebene Zeitpunkt lässt sich nicht eindeutig zu UTC konvertieren
LastEdits: Letzte Änderungen
EditedBy name@Text time@Text: Durch #{name} um #{time}

9
models
View File

@ -4,8 +4,11 @@ User json
matrikelnummer Text Maybe matrikelnummer Text Maybe
email Text email Text
displayName Text displayName Text
maxFavourites Int default=12 maxFavourites Int default=12
theme Theme default='default' theme Theme default='Default'
dateTimeFormat DateTimeFormat "default='%a %d %b %Y %R'"
dateFormat DateTimeFormat "default='%d.%m.%Y'"
timeFormat DateTimeFormat "default='%R'"
UniqueAuthentication plugin ident UniqueAuthentication plugin ident
UniqueEmail email UniqueEmail email
deriving Show deriving Show
@ -67,7 +70,7 @@ Course
registerTo UTCTime Maybe registerTo UTCTime Maybe
deregisterUntil UTCTime Maybe deregisterUntil UTCTime Maybe
registerSecret Text Maybe -- Falls ein Passwort erforderlich ist registerSecret Text Maybe -- Falls ein Passwort erforderlich ist
materialFree Bool default=true materialFree Bool
CourseTermShort term shorthand CourseTermShort term shorthand
CourseEdit CourseEdit
user UserId user UserId

View File

@ -83,6 +83,9 @@ dependencies:
- MonadRandom - MonadRandom
- email-validate - email-validate
- scientific - scientific
- tz
- system-locale
- th-lift-instances
# 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.

4
routes
View File

@ -64,10 +64,10 @@
/subs SSubsR GET POST /subs SSubsR GET POST
/subs/new SubmissionNewR GET POST !timeANDregistered /subs/new SubmissionNewR GET POST !timeANDregistered
/subs/own SubmissionOwnR GET !free -- just redirect /subs/own SubmissionOwnR GET !free -- just redirect
/sub/#CryptoFileNameSubmission SubmissionR !corrector: /sub/#CryptoFileNameSubmission SubmissionR !correctorANDisRead:
/ SubShowR GET POST !ownerANDtime !ownerANDisRead / SubShowR GET POST !ownerANDtime !ownerANDisRead
/archive/#{ZIPArchiveName SubmissionFileType} SubArchiveR GET !owner /archive/#{ZIPArchiveName SubmissionFileType} SubArchiveR GET !owner
/correction CorrectionR GET POST !ownerANDisRead /correction CorrectionR GET POST !corrector !ownerANDisRead
!/#SubmissionFileType/*FilePath SubDownloadR GET !owner !/#SubmissionFileType/*FilePath SubDownloadR GET !owner
/correctors SCorrR GET POST /correctors SCorrR GET POST
!/#SheetFileType/*FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector !/#SheetFileType/*FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector

View File

@ -72,8 +72,8 @@ import System.FilePath
import Handler.Utils.Templates import Handler.Utils.Templates
import Handler.Utils.StudyFeatures import Handler.Utils.StudyFeatures
import Handler.Utils.DateTime
import Control.Lens import Control.Lens
import Utils
import Utils.Lens import Utils.Lens
import Data.Aeson import Data.Aeson
@ -88,9 +88,6 @@ import Text.Shakespeare.Text (st)
instance DisplayAble TermId where instance DisplayAble TermId where
display = termToText . unTermKey display = termToText . unTermKey
instance DisplayAble UTCTime where
display = pack . formatTimeGerDT2 -- default Time Format to be used: 00.00.00 00:00
instance (PathPiece b) => DisplayAble (Data.CryptoID.CryptoID a b) where instance (PathPiece b) => DisplayAble (Data.CryptoID.CryptoID a b) where
display = toPathPiece -- requires import of Data.CryptoID here display = toPathPiece -- requires import of Data.CryptoID here
-- -- MOVE ABOVE -- -- MOVE ABOVE
@ -184,6 +181,13 @@ instance RenderMessage UniWorX SheetFileType where
SheetMarking -> renderMessage' MsgSheetMarking SheetMarking -> renderMessage' MsgSheetMarking
where renderMessage' = renderMessage foundation ls where renderMessage' = renderMessage foundation ls
getTimeLocale' :: [Lang] -> TimeLocale
getTimeLocale' = $(timeLocaleMap [("de", "de_DE.utf8")])
appTZ :: TZ
appTZ = $(includeSystemTZ "Europe/Berlin")
-- Access Control -- Access Control
data AccessPredicate data AccessPredicate
= APPure (Route UniWorX -> Bool -> Reader MsgRenderer AuthResult) = APPure (Route UniWorX -> Bool -> Reader MsgRenderer AuthResult)
@ -600,7 +604,7 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb UsersR = return ("Benutzer", Just HomeR) breadcrumb UsersR = return ("Benutzer", Just HomeR)
breadcrumb AdminTestR = return ("Test" , Just HomeR) breadcrumb AdminTestR = return ("Test" , Just HomeR)
breadcrumb (AdminUserR _) = return ("Users" , Just UsersR) breadcrumb (AdminUserR _) = return ("Users" , Just UsersR)
breadcrumb VersionR = return ("Info" , Just HomeR) breadcrumb VersionR = return ("Impressum" , Just HomeR)
breadcrumb ProfileR = return ("Profile" , Just HomeR) breadcrumb ProfileR = return ("Profile" , Just HomeR)
breadcrumb ProfileDataR = return ("Data" , Just ProfileR) breadcrumb ProfileDataR = return ("Data" , Just ProfileR)
@ -999,9 +1003,14 @@ instance YesodAuth UniWorX where
userEmail <- maybe (throwError $ ServerError "Could not retrieve user email") return userEmail' userEmail <- maybe (throwError $ ServerError "Could not retrieve user email") return userEmail'
userDisplayName <- maybe (throwError $ ServerError "Could not retrieve user name") return userDisplayName' userDisplayName <- maybe (throwError $ ServerError "Could not retrieve user name") return userDisplayName'
AppSettings{..} <- getsYesod appSettings
let let
userMaxFavourites = 12 -- TODO: appDefaultFavourites appSettings userMaxFavourites = appDefaultMaxFavourites
userTheme = Default -- TODO: appDefaultFavourites appSettings userTheme = appDefaultTheme
userDateTimeFormat = appDefaultDateTimeFormat
userDateFormat = appDefaultDateFormat
userTimeFormat = appDefaultTimeFormat
newUser = User{..} newUser = User{..}
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
, UserDisplayName =. userDisplayName , UserDisplayName =. userDisplayName

View File

@ -62,8 +62,8 @@ getTermCourseListR tid = do
[ sortable (Just "shorthand") (textCell MsgCourse) $ anchorCell' [ sortable (Just "shorthand") (textCell MsgCourse) $ anchorCell'
(\(Entity _ Course{..}, _) -> CourseR courseTerm courseShorthand CShowR) (\(Entity _ Course{..}, _) -> CourseR courseTerm courseShorthand CShowR)
(\(Entity _ Course{..}, _) -> toWidget courseShorthand) (\(Entity _ Course{..}, _) -> toWidget courseShorthand)
, sortable (Just "register-from") (textCell MsgRegisterFrom) $ \(Entity _ Course{..}, _) -> textCell $ display courseRegisterFrom , sortable (Just "register-from") (textCell MsgRegisterFrom) $ \(Entity _ Course{..}, _) -> cell $ traverse (formatTime SelFormatDateTime) courseRegisterFrom >>= maybe mempty toWidget
, sortable (Just "register-to") (textCell MsgRegisterTo) $ \(Entity _ Course{..}, _) -> textCell $ display courseRegisterTo , sortable (Just "register-to") (textCell MsgRegisterTo) $ \(Entity _ Course{..}, _) -> cell $ traverse (formatTime SelFormatDateTime) courseRegisterTo >>= maybe mempty toWidget
, sortable (Just "members") (textCell MsgCourseMembers) $ \(Entity _ Course{..}, E.Value num) -> textCell $ case courseCapacity of , sortable (Just "members") (textCell MsgCourseMembers) $ \(Entity _ Course{..}, E.Value num) -> textCell $ case courseCapacity of
Nothing -> MsgCourseMembersCount num Nothing -> MsgCourseMembersCount num
Just max -> MsgCourseMembersCountLimited num max Just max -> MsgCourseMembersCountLimited num max
@ -110,6 +110,8 @@ getCShowR tid csh = do
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 csh CRegisterR) True registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid csh CRegisterR) True
mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom 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")

View File

@ -16,7 +16,7 @@ import Handler.Utils
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Time import Data.Time hiding (formatTime)
-- import qualified Data.Text as T -- import qualified Data.Text as T
-- import Yesod.Form.Bootstrap3 -- import Yesod.Form.Bootstrap3
@ -73,7 +73,7 @@ homeAnonymous = do
, sortable (Just "term") (textCell MsgTerm) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> , sortable (Just "term") (textCell MsgTerm) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } ->
textCell $ display $ courseTerm course textCell $ display $ courseTerm course
, sortable (Just "deadline") (textCell MsgRegisterTo) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> , sortable (Just "deadline") (textCell MsgRegisterTo) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } ->
textCell $ display $ courseRegisterTo course cell $ traverse (formatTime SelFormatDateTime) (courseRegisterTo course) >>= maybe mempty toWidget
] ]
courseTable <- dbTable def $ DBTable courseTable <- dbTable def $ DBTable
{ dbtSQLQuery = tableData { dbtSQLQuery = tableData
@ -150,7 +150,7 @@ homeUser uid = do
, sortable (Just "sheet") (textCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _, _) } -> , sortable (Just "sheet") (textCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _, _) } ->
cell [whamlet|<a href=@{CSheetR tid csh shn SShowR}>#{display shn}|] cell [whamlet|<a href=@{CSheetR tid csh shn SShowR}>#{display shn}|]
, sortable (Just "deadline") (textCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, E.Value deadline, _) } -> , sortable (Just "deadline") (textCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, E.Value deadline, _) } ->
textCell $ display deadline cell $ formatTime SelFormatDateTime deadline >>= toWidget
, sortable (Just "done") (textCell MsgDone) $ \(DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _, E.Value mbsid) }) -> , sortable (Just "done") (textCell MsgDone) $ \(DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _, E.Value mbsid) }) ->
case mbsid of case mbsid of
Nothing -> mempty Nothing -> mempty

View File

@ -22,6 +22,9 @@ import Database.Esqueleto ((^.))
data SettingsForm = SettingsForm data SettingsForm = SettingsForm
{ stgMaxFavourties :: Int { stgMaxFavourties :: Int
, stgTheme :: Theme , stgTheme :: Theme
, stgDateTime :: DateTimeFormat
, stgDate :: DateTimeFormat
, stgTime :: DateTimeFormat
} }
makeSettingForm :: Maybe SettingsForm -> Form SettingsForm makeSettingForm :: Maybe SettingsForm -> Form SettingsForm
@ -32,6 +35,9 @@ makeSettingForm template = identForm FIDsettings $ \html -> do
(fslpI MsgFavoriten "Anzahl Favoriten") (stgMaxFavourties <$> template) (fslpI MsgFavoriten "Anzahl Favoriten") (stgMaxFavourties <$> template)
<*> areq (selectFieldList themeList) <*> areq (selectFieldList themeList)
(fslpI MsgTheme "theme-select" ) (stgTheme <$> template) -- TODO: pass theme-select as id-attribute or similar. (fslpI MsgTheme "theme-select" ) (stgTheme <$> template) -- TODO: pass theme-select as id-attribute or similar.
<*> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) (stgDateTime <$> template)
<*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) (stgDate <$> template)
<*> areq (selectField $ dateTimeFormatOptions SelFormatTime) (fslI MsgTimeFormat) (stgTime <$> template)
<* submitButton <* submitButton
return (result, widget) -- no validation required here return (result, widget) -- no validation required here
@ -43,13 +49,19 @@ getProfileR = do
let settingsTemplate = Just $ SettingsForm let settingsTemplate = Just $ SettingsForm
{ stgMaxFavourties = userMaxFavourites { stgMaxFavourties = userMaxFavourites
, stgTheme = userTheme , stgTheme = userTheme
, stgDateTime = userDateTimeFormat
, stgDate = userDateFormat
, stgTime = userTimeFormat
} }
((res,formWidget), formEnctype) <- runFormPost $ makeSettingForm settingsTemplate ((res,formWidget), formEnctype) <- runFormPost $ makeSettingForm settingsTemplate
case res of case res of
(FormSuccess SettingsForm{..}) -> do (FormSuccess SettingsForm{..}) -> do
runDB $ do runDB $ do
update uid [ UserMaxFavourites =. stgMaxFavourties update uid [ UserMaxFavourites =. stgMaxFavourties
, UserTheme =. stgTheme , UserTheme =. stgTheme
, UserDateTimeFormat =. stgDateTime
, UserDateFormat =. stgDate
, UserTimeFormat =. stgTime
] ]
when (stgMaxFavourties < userMaxFavourites) $ do when (stgMaxFavourties < userMaxFavourites) $ do
-- prune Favourites to user-defined size -- prune Favourites to user-defined size

View File

@ -165,8 +165,8 @@ getSheetListR tid csh = do
let tid = courseTerm course let tid = courseTerm course
let colBase = mconcat let colBase = mconcat
[ headed "Blatt" $ \(sid,sheet,_) -> simpleLink (toWgt $ sheetName sheet) $ CSheetR tid csh (sheetName sheet) SShowR [ headed "Blatt" $ \(sid,sheet,_) -> simpleLink (toWgt $ sheetName sheet) $ CSheetR tid csh (sheetName sheet) SShowR
, headed "Abgabe ab" $ toWgt . formatTimeGerWD . sheetActiveFrom . snd3 , headed "Abgabe ab" $ \(_,Sheet{..},_) -> formatTime SelFormatDateTime sheetActiveFrom >>= toWidget
, headed "Abgabe bis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3 , headed "Abgabe bis" $ \(_,Sheet{..},_) -> formatTime SelFormatDateTime sheetActiveTo >>= toWidget
, headed "Bewertung" $ toWgt . display . sheetType . snd3 , headed "Bewertung" $ toWgt . display . sheetType . snd3
] ]
let colAdmin = mconcat -- only show edit button for allowed course assistants let colAdmin = mconcat -- only show edit button for allowed course assistants
@ -227,7 +227,7 @@ getSShowR tid csh shn = do
SheetExercise -> textCell $ display $ sheetActiveFrom sheet SheetExercise -> textCell $ display $ sheetActiveFrom sheet
SheetHint -> textCell $ display $ sheetHintFrom sheet SheetHint -> textCell $ display $ sheetHintFrom sheet
SheetSolution -> textCell $ display $ sheetSolutionFrom sheet SheetSolution -> textCell $ display $ sheetSolutionFrom sheet
, sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> stringCell $ formatTimeGerWDT (modified :: UTCTime) , sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> cell $ formatTime SelFormatDateTime (modified :: UTCTime) >>= toWidget
] ]
fileTable <- dbTable def $ DBTable fileTable <- dbTable def $ DBTable
{ dbtSQLQuery = fileData { dbtSQLQuery = fileData
@ -249,8 +249,9 @@ getSShowR tid csh shn = do
} }
defaultLayout $ do defaultLayout $ do
setTitle $ toHtml $ T.append "Übung " $ sheetName sheet setTitle $ toHtml $ T.append "Übung " $ sheetName sheet
sheetFrom <- formatTime SelFormatDateTime $ sheetActiveFrom sheet
sheetTo <- formatTime SelFormatDateTime $ sheetActiveTo sheet
$(widgetFile "sheetShow") $(widgetFile "sheetShow")
[whamlet| Under Construction !!! |] -- TODO
getSFileR :: TermId -> Text -> Text -> SheetFileType -> FilePath -> Handler TypedContent getSFileR :: TermId -> Text -> Text -> SheetFileType -> FilePath -> Handler TypedContent
getSFileR tid csh shn typ title = do getSFileR tid csh shn typ title = do

View File

@ -160,7 +160,7 @@ submissionHelper tid csh shn (SubmissionMode mcid) = do
E.orderBy [E.desc $ submissionEdit E.^. SubmissionEditTime] E.orderBy [E.desc $ submissionEdit E.^. SubmissionEditTime]
E.limit numberOfSubmissionEditDates E.limit numberOfSubmissionEditDates
return $ (user E.^. UserDisplayName, submissionEdit E.^. SubmissionEditTime) return $ (user E.^. UserDisplayName, submissionEdit E.^. SubmissionEditTime)
let lastEdits = map (bimap E.unValue E.unValue) lastEditValues lastEdits <- forM lastEditValues $ \(E.Value name, E.Value time) -> (name, ) <$> formatTime SelFormatDateTime time
return (sheet,buddies,lastEdits) return (sheet,buddies,lastEdits)
let unpackZips = True -- undefined -- TODO let unpackZips = True -- undefined -- TODO
((res,formWidget), formEnctype) <- runFormPost $ makeSubmissionForm msmid unpackZips sheetGrouping $ map E.unValue buddies ((res,formWidget), formEnctype) <- runFormPost $ makeSubmissionForm msmid unpackZips sheetGrouping $ map E.unValue buddies
@ -246,6 +246,8 @@ submissionHelper tid csh shn (SubmissionMode mcid) = do
Nothing -> return () Nothing -> return ()
actionUrl <- Data.Maybe.fromJust <$> getCurrentRoute actionUrl <- Data.Maybe.fromJust <$> getCurrentRoute
maySubmit <- (== Authorized) <$> isAuthorized actionUrl True
-- Maybe construct a table to display uploaded archive files -- Maybe construct a table to display uploaded archive files
let colonnadeFiles :: _ -> Colonnade Sortable _ (DBCell (WidgetT UniWorX IO) ()) let colonnadeFiles :: _ -> Colonnade Sortable _ (DBCell (WidgetT UniWorX IO) ())
colonnadeFiles cid = mconcat colonnadeFiles cid = mconcat
@ -355,7 +357,8 @@ getSubArchiveR tid csh shn cID@CryptoID{..} (ZIPArchiveName sfType) = do
fileSource' = do fileSource' = do
fileSource .| Conduit.map entityVal fileSource .| Conduit.map entityVal
maybe (return ()) (yieldM . ratingFile cID) rating when (sfType == SubmissionCorrected) $
maybe (return ()) (yieldM . ratingFile cID) rating
zipComment = Text.encodeUtf8 . pack $ CI.foldedCase ciphertext zipComment = Text.encodeUtf8 . pack $ CI.foldedCase ciphertext

View File

@ -46,19 +46,25 @@ getTermShowR = do
anchorCell' (\(Entity tid _, _) -> TermCourseListR tid) anchorCell' (\(Entity tid _, _) -> TermCourseListR tid)
(\(Entity tid _, _) -> [whamlet|#{display tid}|]) (\(Entity tid _, _) -> [whamlet|#{display tid}|])
, sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) -> , sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureStart cell $ formatTime SelFormatDate termLectureStart >>= toWidget
, sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) -> , sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureEnd 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{..},_) ->
stringCell $ formatTimeGerWD termStart cell $ formatTime SelFormatDate termStart >>= toWidget
, sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) -> , sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termEnd cell $ formatTime SelFormatDate termEnd >>= toWidget
, sortable Nothing "Feiertage im Semester" $ \(Entity _ Term{..},_) -> , sortable Nothing "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays cell $ do
termHolidays' <- mapM (formatTime SelFormatDate) termHolidays
[whamlet|
<ul .list--inline .list--comma-separated>
$forall holiday <- termHolidays'
<li>#{holiday}
|]
] ]
-- let adminColonnade = -- let adminColonnade =
-- [ sortable Nothing "Edit" $ \(Entity tid Term{..},_) -> cell $ do -- [ sortable Nothing "Edit" $ \(Entity tid Term{..},_) -> cell $ do

View File

@ -34,6 +34,12 @@ getUsersR = do
, sortable (Just "display-name") (i18nCell MsgName) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM , sortable (Just "display-name") (i18nCell MsgName) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM
(AdminUserR <$> encrypt uid) (AdminUserR <$> encrypt uid)
(toWidget . display $ userDisplayName) (toWidget . display $ userDisplayName)
, sortable (Just "matriculation") (i18nCell MsgMatrikelNr) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM
(AdminUserR <$> encrypt uid)
(toWidget . display $ userMatrikelnummer)
-- , sortable (Just "last-name") (i18nCell MsgName) $ \DBRow{ dbrOutput = Entity uid User{..} } -> anchorCellM
-- (AdminUserR <$> encrypt uid)
-- (toWidget . display $ last $ impureNonNull $ words $ userDisplayName)
, sortable Nothing (i18nCell MsgAdminFor) $ \DBRow{ dbrOutput = Entity uid _ } -> mempty , sortable Nothing (i18nCell MsgAdminFor) $ \DBRow{ dbrOutput = Entity uid _ } -> mempty
{ dbCellContents = do { dbCellContents = do
schools <- E.select . E.from $ \(school `E.InnerJoin` userAdmin) -> do schools <- E.select . E.from $ \(school `E.InnerJoin` userAdmin) -> do
@ -78,6 +84,12 @@ getUsersR = do
[ ( "display-name" [ ( "display-name"
, SortColumn $ \user -> user E.^. UserDisplayName , SortColumn $ \user -> user E.^. UserDisplayName
) )
, ( "matriculation"
, SortColumn $ \user -> user E.^. UserMatrikelnummer
)
-- , ( "last-name"
-- , SortColumn $ \user -> (last . impureNonNull . words) <$> (user E.^. UserDisplayName)
-- )
] ]
, dbtFilter = mempty , dbtFilter = mempty
, dbtStyle = def , dbtStyle = def

View File

@ -1,60 +1,127 @@
module Handler.Utils.DateTime where {-# LANGUAGE NoImplicitPrelude
, TemplateHaskell
, OverloadedStrings
, RecordWildCards
, TypeFamilies
#-}
import Data.Time module Handler.Utils.DateTime
( utcToLocalTime
, localTimeToUTC, TZ.LocalToUTCResult(..)
, formatTime'
, formatTime
, getTimeLocale, getDateTimeFormat
, validDateTimeFormats, dateTimeFormatOptions
) where
import Import
germanTimeLocale :: TimeLocale import Data.Time.Zones hiding (localTimeToUTCFull)
germanTimeLocale = TimeLocale import qualified Data.Time.Zones as TZ
{ wDays = [("Sonntag" ,"So")
,("Montag" ,"Mo")
,("Dienstag" ,"Di")
,("Mittwoch" ,"Mi")
,("Donnerstag" ,"Do")
,("Freitag" ,"Fr")
,("Samstag" ,"Sa")
]
, months = [("Januar" ,"Jan")
,("Februar" ,"Feb")
,("März" ,"Mär")
,("April" ,"Apr")
,("Mai" ,"Mai")
,("Juni" ,"Jun")
,("Juli" ,"Jul")
,("August" ,"Aug")
,("September" ,"Sep")
,("Oktober" ,"Okt")
,("November" ,"Nov")
,("Dezember" ,"Dez")
]
, amPm = ("am","pm")
, dateTimeFmt = "%a %e.%m.%y %k:%M"
, dateFmt = "%e.%m.%y"
, timeFmt = "%k:%M"
, time12Fmt = "%H:%M"
, knownTimeZones = [] -- TODO?
}
formatTimeGer :: FormatTime t => String -> t -> String import Data.Time hiding (formatTime, localTimeToUTC, utcToLocalTime)
formatTimeGer = formatTime germanTimeLocale import qualified Data.Time.Format as Time
formatTimeGerDTlong :: FormatTime t => t -> String import Data.Set (Set)
formatTimeGerDTlong = formatTimeGer "%A, %e. %B %Y, %k:%M:%S" import qualified Data.Set as Set
formatTimeGerWDT :: FormatTime t => t -> String utcToLocalTime :: UTCTime -> LocalTime
formatTimeGerWDT = formatTimeGer $ dateTimeFmt germanTimeLocale utcToLocalTime = TZ.utcToLocalTimeTZ appTZ
formatTimeGerDT :: FormatTime t => t -> String -- 0.00.00 0:00 localTimeToUTC :: LocalTime -> LocalToUTCResult
formatTimeGerDT = formatTimeGer "%e.%m.%y %k:%M" -- leading spaces at start, otherwise 0 padding localTimeToUTC = TZ.localTimeToUTCFull appTZ
-- the following is used by DisplayAble's display: class FormatTime t => HasLocalTime t where
formatTimeGerDT2 :: FormatTime t => t -> String -- Day 00.00.00 00:00 toLocalTime :: t -> LocalTime
formatTimeGerDT2 = formatTimeGer "%a %d.%m.%y %H:%M" -- always padding with 0
formatTimeGerWD :: FormatTime t => t -> String instance HasLocalTime LocalTime where
formatTimeGerWD = formatTimeGer "%a %e.%m.%y" toLocalTime = id
formatTimeGerD :: FormatTime t => t -> String instance HasLocalTime Day where
formatTimeGerD = formatTimeGer $ dateFmt germanTimeLocale toLocalTime d = LocalTime d midnight
formatTimeGerT :: FormatTime t => t -> String instance HasLocalTime UTCTime where
formatTimeGerT = formatTimeGer $ timeFmt germanTimeLocale toLocalTime t = utcToLocalTime t
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 :: (FormatTime t, MonadHandler m, HandlerSite m ~ UniWorX, IsString str) => (DateTimeFormat -> String) -> t -> m str
-- Restricted type for safety
formatTime :: (HasLocalTime t, MonadHandler m, HandlerSite m ~ UniWorX) => SelDateTimeFormat -> t -> m Text
formatTime proj t = flip formatTime' t =<< (unDateTimeFormat <$> getDateTimeFormat proj)
getTimeLocale :: (MonadHandler m, HandlerSite m ~ UniWorX) => m TimeLocale
getTimeLocale = getTimeLocale' <$> languages
getDateTimeFormat :: (MonadHandler m, HandlerSite m ~ UniWorX) => SelDateTimeFormat -> m DateTimeFormat
getDateTimeFormat sel = do
mauth <- liftHandlerT maybeAuth
AppSettings{..} <- getsYesod appSettings
let
fmt
| Just (Entity _ User{..}) <- mauth
= case sel of
SelFormatDateTime -> userDateTimeFormat
SelFormatDate -> userDateFormat
SelFormatTime -> userTimeFormat
| otherwise
= case sel of
SelFormatDateTime -> appDefaultDateTimeFormat
SelFormatDate -> appDefaultDateFormat
SelFormatTime -> appDefaultTimeFormat
return fmt
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
validDateTimeFormats _ SelFormatDateTime = Set.fromList $
[ DateTimeFormat "%a %d %b %Y %R"
, DateTimeFormat "%a %b %d %Y %R"
, DateTimeFormat "%A, %d %B %Y %R"
, DateTimeFormat "%A, %B %d %Y %R"
, DateTimeFormat "%a %d %b %Y %T"
, DateTimeFormat "%a %b %d %Y %T"
, DateTimeFormat "%A, %d %B %Y %T"
, DateTimeFormat "%A, %B %d %Y %T"
, DateTimeFormat "%d.%m.%Y %R"
, DateTimeFormat "%d.%m.%Y %T"
, DateTimeFormat "%R %d.%m.%Y"
, DateTimeFormat "%T %d.%m.%Y"
, DateTimeFormat "%Y-%m-%d %R"
, DateTimeFormat "%Y-%m-%d %T"
, DateTimeFormat "%Y-%m-%dT%T"
]
validDateTimeFormats _ SelFormatDate = Set.fromList $
[ DateTimeFormat "%a %d %b %Y"
, DateTimeFormat "%a %b %d %Y"
, DateTimeFormat "%A, %d %B %Y"
, DateTimeFormat "%A, %B %d %Y"
, DateTimeFormat "%d.%m.%Y"
, DateTimeFormat "%Y-%m-%d"
]
validDateTimeFormats TimeLocale{..} SelFormatTime = Set.fromList . concat . catMaybes $
[ Just
[ DateTimeFormat "%R"
, DateTimeFormat "%T"
]
, do
guard $ uncurry (/=) amPm
Just
[ DateTimeFormat "%I:%M %p"
, DateTimeFormat "%I:%M %P"
, DateTimeFormat "%I:%M:%S %p"
, DateTimeFormat "%I:%M:%S %P"
]
]
dateTimeFormatOptions :: (MonadHandler m, HandlerSite m ~ UniWorX) => SelDateTimeFormat -> m (OptionList DateTimeFormat)
dateTimeFormatOptions sel = do
now <- liftIO getCurrentTime
tl <- getTimeLocale
let
toOption fmt@DateTimeFormat{..} = do
dateTime <- formatTime' unDateTimeFormat now
return $ (dateTime, fmt)
optionsPairs <=< mapM toOption . Set.toList $ validDateTimeFormats tl sel

View File

@ -17,6 +17,7 @@ import Handler.Utils.Form.Types
import Handler.Utils.Templates import Handler.Utils.Templates
import Handler.Utils.DateTime import Handler.Utils.DateTime
import qualified Data.Time as Time
import Import import Import
import qualified Data.Char as Char import qualified Data.Char as Char
@ -404,15 +405,16 @@ dayTimeField fs mutc = do
-} -}
utcTimeField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m UTCTime utcTimeField :: (MonadHandler m, HandlerSite m ~ UniWorX) => Field m UTCTime
-- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing) -- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing)
-- TODO: Verify whether this is UTC or local time from Browser -- Browser returns LocalTime
utcTimeField = Field utcTimeField = Field
{ fieldParse = parseHelper $ readTime { fieldParse = parseHelperGen $ readTime
, fieldView = \theId name attrs val isReq -> , fieldView = \theId name attrs val isReq -> do
val' <- either id id <$> traverse (formatTime' fieldTimeFormat) val
[whamlet| [whamlet|
$newline never $newline never
<input id="#{theId}" name="#{name}" *{attrs} type="datetime-local" :isReq:required value="#{either id showTime val}"> <input id="#{theId}" name="#{name}" *{attrs} type="datetime-local" :isReq:required value="#{val'}">
|] |]
, fieldEnctype = UrlEncoded , fieldEnctype = UrlEncoded
} }
@ -420,15 +422,15 @@ utcTimeField = Field
fieldTimeFormat :: String fieldTimeFormat :: String
--fieldTimeFormat = "%e.%m.%y %k:%M" --fieldTimeFormat = "%e.%m.%y %k:%M"
fieldTimeFormat = "%Y-%m-%dT%H:%M" fieldTimeFormat = "%Y-%m-%dT%H:%M"
readTime :: Text -> Either FormMessage UTCTime -- `defaultTimeLocale` is okay here, since `fieldTimeFormat` does not contain any
readTime :: Text -> Either UniWorXMessage UTCTime
readTime t = readTime t =
case parseTimeM True germanTimeLocale fieldTimeFormat (T.unpack t) of case localTimeToUTC <$> parseTimeM True defaultTimeLocale fieldTimeFormat (T.unpack t) of
(Just time) -> Right time (Just (LTUUnique time _)) -> Right time
Nothing -> Left $ MsgInvalidEntry $ "Datum/Zeit Format: tt.mm.yy hh:mm " ++ t (Just (LTUNone time _)) -> Right time -- FIXME: Should this be an error, too?
(Just (LTUAmbiguous _ _ _ _)) -> Left MsgAmbiguousUTCTime
showTime :: UTCTime -> Text Nothing -> Left MsgInvalidDateTimeFormat
showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat)
fsm :: RenderMessage UniWorX msg => msg -> FieldSettings UniWorX -- DEPRECATED fsm :: RenderMessage UniWorX msg => msg -> FieldSettings UniWorX -- DEPRECATED

View File

@ -117,7 +117,7 @@ formatRating cID Rating{ ratingValues = Rating'{..}, ..} = let
doc = renderPretty 1 45 $ foldr (<$$>) mempty doc = renderPretty 1 45 $ foldr (<$$>) mempty
[ "= Bitte nur Bewertung und Kommentare ändern =" [ "= Bitte nur Bewertung und Kommentare ändern ="
, "=============================================" , "============================================="
, "========== UniWorx Bewertungsdatei ==========" , "========== Uni2work Bewertungsdatei =========="
, "======= diese Datei ist UTF8 encodiert ======" , "======= diese Datei ist UTF8 encodiert ======"
, "Informationen zum Übungsblatt:" , "Informationen zum Übungsblatt:"
, indent 2 $ foldr (<$$>) mempty , indent 2 $ foldr (<$$>) mempty

View File

@ -3,20 +3,19 @@ module Import.NoFoundation
( module Import ( module Import
) where ) where
import ClassyPrelude.Yesod as Import import ClassyPrelude.Yesod as Import hiding (formatTime)
import Model as Import import Model as Import
import Settings as Import import Settings as Import
import Settings.StaticFiles as Import import Settings.StaticFiles as Import
import Yesod.Auth as Import import Yesod.Auth as Import
import Yesod.Core.Types as Import (loggerSet) import Yesod.Core.Types as Import (loggerSet)
import Yesod.Default.Config2 as Import import Yesod.Default.Config2 as Import
import Utils as Import import Utils as Import
import Data.Fixed as Import import Data.Fixed as Import
import CryptoID as Import import CryptoID as Import
import Data.UUID as Import (UUID) import Data.UUID as Import (UUID)
import Text.Lucius as Import
import Text.Lucius as Import

View File

@ -3,7 +3,7 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric, DeriveDataTypeable, GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-- # LANGUAGE ExistentialQuantification #-} -- for DA type {-- # LANGUAGE ExistentialQuantification #-} -- for DA type
@ -285,12 +285,15 @@ instance Default Theme where
derivePersistField "Theme" derivePersistField "Theme"
newtype ZIPArchiveName obj = ZIPArchiveName obj newtype ZIPArchiveName obj = ZIPArchiveName { unZIPArchiveName :: obj }
deriving (Show, Read, Eq) deriving (Show, Read, Eq)
instance PathPiece obj => PathPiece (ZIPArchiveName obj) where instance PathPiece obj => PathPiece (ZIPArchiveName obj) where
fromPathPiece (map CI.mk . unpack -> s) fromPathPiece = fmap ZIPArchiveName . fromPathPiece <=< (stripSuffix `on` CI.foldCase) ".zip"
| Just s' <- stripSuffix (map CI.mk ".zip") s = fromPathPiece . pack $ map CI.original s' toPathPiece = (<> ".zip") . toPathPiece . unZIPArchiveName
| otherwise = Nothing
toPathPiece (ZIPArchiveName obj) = toPathPiece obj <> ".zip" newtype DateTimeFormat = DateTimeFormat { unDateTimeFormat :: String }
deriving (Eq, Ord, Read, Show, ToJSON, FromJSON, PersistField, PersistFieldSql)
data SelDateTimeFormat = SelFormatDateTime | SelFormatDate | SelFormatTime
deriving (Eq, Ord, Read, Show, Enum, Bounded)

View File

@ -24,6 +24,8 @@ import Yesod.Default.Util (WidgetFileSettings,
widgetFileNoReload, widgetFileNoReload,
widgetFileReload) widgetFileReload)
import Model
-- | Runtime settings to configure this application. These settings can be -- | Runtime settings to configure this application. These settings can be
-- loaded from various sources: defaults, environment variables, config files, -- loaded from various sources: defaults, environment variables, config files,
-- theoretically even a database. -- theoretically even a database.
@ -59,8 +61,11 @@ data AppSettings = AppSettings
, appSkipCombining :: Bool , appSkipCombining :: Bool
-- ^ Perform no stylesheet/script combining -- ^ Perform no stylesheet/script combining
, appDefaultFavourites :: Int , appDefaultTheme :: Theme
-- ^ Initial Value for remembered Favourites , appDefaultMaxFavourites :: Int
, appDefaultDateTimeFormat :: DateTimeFormat
, appDefaultDateFormat :: DateTimeFormat
, appDefaultTimeFormat :: DateTimeFormat
-- Example app-specific configuration values. -- Example app-specific configuration values.
, appCopyright :: Text , appCopyright :: Text
@ -75,6 +80,7 @@ data AppSettings = AppSettings
-- ^ If set authenticate against a local password file -- ^ If set authenticate against a local password file
, appAllowDeprecated :: Bool , appAllowDeprecated :: Bool
-- ^ Indicate if deprecated routes are accessible for everyone -- ^ Indicate if deprecated routes are accessible for everyone
} }
instance FromJSON AppSettings where instance FromJSON AppSettings where
@ -101,7 +107,11 @@ instance FromJSON AppSettings where
appMutableStatic <- o .:? "mutable-static" .!= defaultDev appMutableStatic <- o .:? "mutable-static" .!= defaultDev
appSkipCombining <- o .:? "skip-combining" .!= defaultDev appSkipCombining <- o .:? "skip-combining" .!= defaultDev
appDefaultFavourites <- o .: "userDefaultFavourites" appDefaultMaxFavourites <- o .: "default-favourites"
appDefaultTheme <- o .: "default-theme"
appDefaultDateTimeFormat <- o .: "default-date-time-format"
appDefaultDateFormat <- o .: "default-date-format"
appDefaultTimeFormat <- o .: "default-time-format"
appCopyright <- o .: "copyright" appCopyright <- o .: "copyright"
appAnalytics <- o .:? "analytics" appAnalytics <- o .:? "analytics"

View File

@ -19,6 +19,7 @@ import qualified Data.Char as Char
import Utils.DB as Utils import Utils.DB as Utils
import Utils.Common as Utils import Utils.Common as Utils
import Utils.DateTime as Utils
import Text.Blaze (Markup, ToMarkup) import Text.Blaze (Markup, ToMarkup)

57
src/Utils/DateTime.hs Normal file
View File

@ -0,0 +1,57 @@
{-# LANGUAGE NoImplicitPrelude
, TemplateHaskell
, QuasiQuotes
, StandaloneDeriving
, DeriveLift
#-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Utils.DateTime
( timeLocaleMap
, TimeLocale(..)
, module Data.Time.Zones
, module Data.Time.Zones.TH
) where
import ClassyPrelude.Yesod hiding (lift)
import System.Locale.Read
import Data.Time (TimeZone(..), TimeLocale(..))
import Data.Time.Zones (TZ)
import Data.Time.Zones.TH (includeSystemTZ)
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (Lift(..))
import Instances.TH.Lift ()
deriving instance Lift TimeZone
deriving instance Lift TimeLocale
-- $(timeLocaleMap _) :: [Lang] -> TimeLocale
timeLocaleMap :: [(Lang, String)] -- ^ Languages and matching locales, first is taken as default
-> ExpQ
timeLocaleMap [] = fail "Need at least one (language, locale)-pair"
timeLocaleMap extra@((_, defLocale):_) = do
localeMap <- newName "localeMap"
let
localeMap' = funD localeMap $ map matchLang extra ++ [reduceLangList, defaultLang]
defaultLang :: ClauseQ
defaultLang =
clause [listP []] (normalB $ localeExp defLocale) []
reduceLangList :: ClauseQ
reduceLangList = do
ls <- newName "ls"
clause [[p|(_ : $(varP ls))|]] (normalB [e|$(varE localeMap) $(varE ls)|]) []
matchLang :: (Lang, String) -> ClauseQ
matchLang (lang, localeStr) = do
lang' <- newName "lang"
clause [[p|($(varP lang') : _)|]] (guardedB [(,) <$> normalG [e|$(varE lang') == lang|] <*> localeExp localeStr]) []
localeExp :: String -> ExpQ
localeExp = lift <=< runIO . getLocale . Just
letE [localeMap'] (varE localeMap)

View File

@ -3,10 +3,10 @@
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE FunctionalDependencies #-}
module Utils.Lens where module Utils.Lens ( module Utils.Lens ) where
import Import.NoFoundation import Import.NoFoundation
import Control.Lens import Control.Lens as Utils.Lens
makeClassy_ ''Entity makeClassy_ ''Entity

View File

@ -25,6 +25,10 @@ packages:
git: https://github.com/pngwjpgh/encoding.git git: https://github.com/pngwjpgh/encoding.git
commit: 67bb87ceff53f0178c988dd4e15eeb2daee92b84 commit: 67bb87ceff53f0178c988dd4e15eeb2daee92b84
extra-dep: true extra-dep: true
- location:
git: https://github.com/pngwjpgh/system-locale.git
commit: d803ce3607ac6813ac1a065acb423220f57dab3c
extra-dep: true
extra-deps: extra-deps:
- colonnade-1.2.0 - colonnade-1.2.0

View File

@ -22,13 +22,13 @@
#{participants} #{participants}
$maybe capacity <- courseCapacity course $maybe capacity <- courseCapacity course
\ von #{capacity} \ von #{capacity}
$maybe regFrom <- courseRegisterFrom course $maybe regFrom <- mRegFrom
<dt .deflist__dt>Anmeldezeitraum <dt .deflist__dt>Anmeldezeitraum
<dd .deflist__dd> <dd .deflist__dd>
<div> <div>
Ab #{formatTimeGerWD regFrom} Ab #{regFrom}
$maybe regTo <- courseRegisterTo course $maybe regTo <- mRegTo
\ bis #{formatTimeGerWD regTo} \ bis #{regTo}
$if registrationOpen $if registrationOpen
<dt .deflist__dt> <dt .deflist__dt>
<dd .deflist__dd> <dd .deflist__dd>

View File

@ -24,6 +24,7 @@
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
margin: 0; margin: 0;
-webkit-font-smoothing: antialiased;
} }
body { body {
@ -152,13 +153,16 @@ h4 {
/* LAYOUT */ /* LAYOUT */
.main { .main {
position: relative;
min-height: calc(100vh - var(--header-height)); min-height: calc(100vh - var(--header-height));
padding: 20px;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.main { .main {
min-height: calc(100vh - var(--header-height-collapsed)); min-height: calc(100vh - var(--header-height-collapsed));
padding: 10px;
} }
} }
@ -166,7 +170,6 @@ h4 {
position: relative; position: relative;
background-color: white; background-color: white;
overflow: hidden; overflow: hidden;
padding-left: 24%;
transition: padding-left .2s ease-out; transition: padding-left .2s ease-out;
> .container { > .container {
@ -182,42 +185,36 @@ h4 {
} }
} }
@media (max-width: 768px) {
.logged-in {
.main__content {
padding-left: 60px;
}
}
}
@media (max-width: 425px) {
.logged-in {
.main__content {
padding-left: 0;
}
}
}
@media (min-width: 769px) {
.logged-in {
.main__content {
padding-left: calc(24% + 30px);
}
}
}
@media (min-width: 1200px) { @media (min-width: 1200px) {
.main__content { .logged-in {
padding-left: 300px; .main__content {
} padding-left: 320px;
} }
@media (max-width: 768px) {
.main__content {
padding-left: 50px;
}
}
@media (max-width: 425px) {
.main__content {
padding-left: 0;
}
}
.main__content-body {
padding: 30px 40px 60px;
}
@media (max-width: 768px) {
.main__content-body {
padding: 30px 20px 60px;
}
}
@media (max-width: 425px) {
.main__content-body {
padding: 20px 10px 60px;
} }
} }
@ -419,7 +416,7 @@ input[type="button"].btn-info:hover,
} }
/* LIST MODIFIERS */ /* LIST MODIFIERS */
.list--inline > li { .list--inline li {
display: inline-block; display: inline-block;
} }

View File

@ -1,30 +1,23 @@
<div .masthead>
<div .container>
<div .row>
<h1 .header>
#{sheetName sheet}
<div .container> <div .container>
<div .bs-docs-section> $maybe descr <- sheetDescription sheet
<div .row> <h2 #description>Hinweise
<div .col-lg-12> <p> #{descr}
<div .page-header>
$maybe descr <- sheetDescription sheet <h3>Bewertung
<h2 #description>Hinweise <p>
<p> #{descr} #{display $ sheetType sheet}
<h3>Bewertung
<p> #{display $ sheetType sheet}
$maybe marking <- sheetMarkingText sheet
<p> #{marking}
<br>
Freigeschaltet ab:
\ #{formatTimeGerWD $ sheetActiveFrom sheet}
\ Abgabe bis:
\ #{formatTimeGerWD $ sheetActiveTo sheet}
<div .row> $maybe marking <- sheetMarkingText sheet
<div .col-lg-12> <p>
<h2>Dateien #{marking}
^{fileTable}
<hr>
<p>
Freigeschaltet ab
#{sheetFrom}
<p>
Abgabe bis
#{sheetTo}
<h2>Dateien
^{fileTable}

View File

@ -43,10 +43,7 @@
padding-left: 20px; padding-left: 20px;
margin-left: 20px; margin-left: 20px;
animation: slide-in-alert .2s ease-out forwards; animation: slide-in-alert .2s ease-out forwards;
margin-bottom: 20px;
+ .alert:not(.alert--invisible) {
margin-top: 20px;
}
&:hover { &:hover {
@ -100,13 +97,14 @@
font-weight: 600; font-weight: 600;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
text-align: left;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.alert__content { .alert__content {
padding: 4px 7px; padding: 4px 7px;
padding-left: 20px; padding-left: 25px;
} }
} }
@ -190,4 +188,6 @@
.alert--invisible { .alert--invisible {
max-height: 0; max-height: 0;
transform: translateX(120%); transform: translateX(120%);
margin-bottom: 0;
overflow: hidden;
} }

View File

@ -59,6 +59,7 @@
resetFileLabel(); resetFileLabel();
input.classList.add('file-input__input--hidden'); input.classList.add('file-input__input--hidden');
input.addEventListener('change', function() { input.addEventListener('change', function() {
input.dispatchEvent(new Event('input'));
if (isMulti) { if (isMulti) {
renderFileList(input.files); renderFileList(input.files);
} }

View File

@ -1,13 +1,18 @@
$maybe cID <- mcid $maybe cID <- mcid
<section style="padding-bottom:1em; margin-bottom:1em; border-bottom:1px solid black;"> <section>
<h2> <h2>
<a href=@{CSubmissionR tid csh shn cID (SubArchiveR (ZIPArchiveName SubmissionCorrected))}>Archiv <a href=@{CSubmissionR tid csh shn cID (SubArchiveR (ZIPArchiveName SubmissionCorrected))}>Archiv
$forall (name,time) <- lastEdits (<a href=@{CSubmissionR tid csh shn cID (SubArchiveR (ZIPArchiveName SubmissionOriginal))}>Original</a>)
<div>last edited by #{name} at #{formatTimeGerDTlong time} $if not (null lastEdits)
<h3>_{MsgLastEdits}
<ul>
$forall (name,time) <- lastEdits
<li>_{MsgEditedBy name time}
$maybe fileTable <- mFileTable $maybe fileTable <- mFileTable
<h3>Enthaltene Dateien: <h3>_{MsgSubmissionFiles}
^{fileTable} ^{fileTable}
<section> $if maySubmit
<form .form-horizontal method=post action=@{actionUrl} enctype=#{formEnctype}> <section>
^{formWidget} <form .form-horizontal method=post action=@{actionUrl} enctype=#{formEnctype}>
^{formWidget}

View File

@ -9,12 +9,12 @@ $newline never
$nothing $nothing
<tbody> <tbody>
$if null wRows && (dbsEmptyStyle == DBESHeading) $if null wRows && (dbsEmptyStyle == DBESHeading)
<tr> <tr .table__row>
<td colspan=#{show columnCount}> <td .table__td colspan=#{show columnCount}>
_{dbsEmptyMessage} _{dbsEmptyMessage}
$else $else
$forall row <- wRows $forall row <- wRows
<tr .table__row> <tr .table__row>
$forall widget <- row $forall widget <- row
$# cell/body.hamlet $# cell/body.hamlet
^{widget} ^{widget}

View File

@ -3,27 +3,21 @@
document.addEventListener('DOMContentLoaded', function DOMContentLoaded() { document.addEventListener('DOMContentLoaded', function DOMContentLoaded() {
var ASC = 'asc';
var DESC = 'desc';
function setupAsync(wrapper) { function setupAsync(wrapper) {
var table = wrapper.querySelector('#' + #{String $ dbtIdent}); var table = wrapper.querySelector('#' + #{String $ dbtIdent});
var ths = Array.from(table.querySelectorAll('th.sortable')); var ths = Array.from(table.querySelectorAll('th.sortable'));
if (ths) {
// attach click handler to each sortable column if any
ths.forEach(function(th) {
th.addEventListener('click', clickHandler);
});
}
var pagination = wrapper.querySelector('#' + #{String $ dbtIdent} + '-pagination'); var pagination = wrapper.querySelector('#' + #{String $ dbtIdent} + '-pagination');
ths.forEach(function(th) {
th.addEventListener('click', clickHandler);
});
if (pagination) { if (pagination) {
var paginationLinks = Array.from(pagination.querySelectorAll('.pagination-link')); Array.from(pagination.querySelectorAll('.pagination-link'))
// attach click handler to pagination links if any .forEach(function(p) {
paginationLinks.forEach(function(p) { p.addEventListener('click', clickHandler);
p.addEventListener('click', clickHandler); });
});
} }
function clickHandler(event) { function clickHandler(event) {
@ -31,12 +25,20 @@
var url = new URL(window.location.origin + window.location.pathname + getClickDestination(this)); var url = new URL(window.location.origin + window.location.pathname + getClickDestination(this));
url.searchParams.set(#{String $ wIdent "table-only"}, 'yes'); url.searchParams.set(#{String $ wIdent "table-only"}, 'yes');
updateTableFrom(url); updateTableFrom(url);
ths.forEach(function(th) {
// th.removeEventListener('click', clickHandler);
console.log('removed handler from', th);
});
} }
function getClickDestination(el) { function getClickDestination(el) {
var link = el.querySelector('a'); console.log(el);
if (!link) { return false; } if (!el.querySelector('a')) {
return link.getAttribute('href'); return false;
}
return el.querySelector('a').getAttribute('href');
} }
// fetches new sorted table from url with params and replaces contents of current table // fetches new sorted table from url with params and replaces contents of current table
@ -47,14 +49,19 @@
'Accept': 'text/html' 'Accept': 'text/html'
} }
}).then(function(response) { }).then(function(response) {
var contentType = response.headers.get("content-type");
if (!response.ok) { if (!response.ok) {
throw ('Looks like there was a problem fetching ' + url.toString() + '. Status Code: ' + response.status); throw ('Looks like there was a problem fetching ' + url.toString() + '. Status Code: ' + response.status);
} }
return response.text(); return response.text();
}).then(function(data) { }).then(function(data) {
// remove listeners
ths.forEach(function(th) {
th.removeEventListener('click', clickHandler);
});
// replace contents of table body // replace contents of table body
wrapper.innerHTML = data; wrapper.innerHTML = data;
// set up async functionality again // set up async functionality again
setupAsync(wrapper); setupAsync(wrapper);
table.querySelector('tbody').innerHTML = data; table.querySelector('tbody').innerHTML = data;

View File

@ -10,6 +10,17 @@
Uni2work ist noch nicht abgeschlossen. Uni2work ist noch nicht abgeschlossen.
^{features} ^{features}
<p>
<h2>
Bekannte Bugs
<ul>
<li>
Umlaute in Benutzernamen werden durch externes LDAP-Plugin entfernt
<li>
Auswahlbox "alle markieren" fehlt noch
<li>
Tabellen über mehrere Seiten müssen vor Seitenwechsel manuell sortiert werden
<p> <p>
<h2> <h2>
Versionsgeschichte Versionsgeschichte

View File

@ -8,6 +8,7 @@
function init() { function init() {
var favoritesBtn = document.querySelector('.navbar__list-item--favorite'); var favoritesBtn = document.querySelector('.navbar__list-item--favorite');
favoritesBtn.addEventListener('click', function(event) { favoritesBtn.addEventListener('click', function(event) {
favoritesBtn.classList.toggle('navbar__list-item--active');
asideEl.classList.toggle('main__aside--expanded'); asideEl.classList.toggle('main__aside--expanded');
event.preventDefault(); event.preventDefault();
}, true); }, true);
@ -19,21 +20,6 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var asidenavEl = document.querySelector('.main__aside'); var asidenavEl = document.querySelector('.main__aside');
var mainContentEl = document.querySelector('.main__content');
function adjustHeight() {
window.requestAnimationFrame(function() {
asidenavEl.style.height = mainContentEl.clientHeight + 'px';
});
}
// unbeknownst to the user (below the fold), this happes slightly delayed
// because of dynamic changes to the styles inside the main__content
setTimeout(function() {
adjustHeight();
}, 10);
window.addEventListener('resize', adjustHeight);
window.utils.aside(asidenavEl); window.utils.aside(asidenavEl);

View File

@ -1,12 +1,22 @@
.main__aside { .main__aside {
position: absolute; position: absolute;
display: none;
background-color: var(--color-dark); background-color: var(--color-dark);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1; z-index: 1;
top: 0;
left: 0;
flex: 0 0 300px; flex: 0 0 300px;
min-height: calc(100% - var(--header-height)); min-height: calc(100% - var(--header-height));
transition: all .2s ease-out; transition: all .2s ease-out;
width: 24%; width: 24%;
height: 100%;
}
.logged-in {
.main__aside {
display: block;
}
} }
/* maximum width of 300px for wide screens */ /* maximum width of 300px for wide screens */
@ -168,7 +178,6 @@
color: var(--color-font); color: var(--color-font);
transform: translateX(0); transform: translateX(0);
opacity: 0; opacity: 0;
transition: all .2s ease-out;
width: 0; width: 0;
overflow: hidden; overflow: hidden;
z-index: -1; z-index: -1;

View File

@ -1,26 +1,9 @@
.breadcrumbs__container { .breadcrumbs__container {
position: relative; position: relative;
color: var(--color-font); color: var(--color-font);
margin-left: 40px;
margin-top: 25px;
margin-bottom: 10px; margin-bottom: 10px;
} }
@media (max-width: 768px) {
.breadcrumbs__container {
margin-left: 20px;
}
}
@media (max-width: 425px) {
.breadcrumbs__container {
margin-left: 10px;
margin-top: 10px;
}
}
.breadcrumbs__link { .breadcrumbs__link {
&:hover { &:hover {

View File

@ -56,7 +56,6 @@
min-width: 70px; min-width: 70px;
height: calc(100% - 4px); height: calc(100% - 4px);
padding: 0 6px 4px; padding: 0 6px 4px;
box-shadow: 0 0 0 1px inset var(--color-lmu-box-border);
} }
&::after { &::after {
@ -70,7 +69,14 @@
width: 100%; width: 100%;
height: calc(100% - 4px); height: calc(100% - 4px);
padding: 0 6px 4px; padding: 0 6px 4px;
box-shadow: 0 0 0 1px inset var(--color-lmu-box-border); }
}
@media (min-width: 769px) {
.navbar__logo::before,
.navbar__logo::after {
border: 1px solid var(--color-lmu-box-border);
} }
} }
@ -114,7 +120,10 @@
min-width: 90px; min-width: 90px;
color: var(--color-lightwhite); color: var(--color-lightwhite);
transition: height .2s cubic-bezier(0.03, 0.43, 0.58, 1); transition: height .2s cubic-bezier(0.03, 0.43, 0.58, 1);
box-shadow: 0 0 0 1px inset var(--color-lmu-box-border);
&:hover {
color: var(--color-lightwhite);
}
} }
.navbar__link-label { .navbar__link-label {
@ -123,6 +132,13 @@
text-transform: uppercase; text-transform: uppercase;
} }
@media (min-width: 769px) {
.navbar__link-wrapper {
border: 1px solid var(--color-lmu-box-border);
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.navbar__link-wrapper { .navbar__link-wrapper {
@ -217,20 +233,33 @@
} }
} }
/* "Favorites" list item, only visible on small screens */ /* "Favorites" list item, only visible on small screens and logged in */
.navbar__list { .navbar__list-item {
&.navbar__list-item--favorite {
.navbar__list-item--favorite {
display: none; display: none;
background-color: var(--color-primary); }
}
.navbar__list-item--favorite {
display: none;
background-color: var(--color-primary);
}
.logged-in {
.navbar__list {
li.navbar__list-item--favorite,
.navbar__list-item--favorite {
display: inline-block;
}
} }
} }
@media (max-width: 425px) { @media (min-width: 426px) {
.navbar__list { .logged-in {
.navbar__list-item--favorite { .navbar__list {
display: inline-block; .navbar__list-item--favorite {
display: none !important;
}
} }
} }
} }
@ -241,7 +270,15 @@
.navbar__link-wrapper { .navbar__link-wrapper {
color: var(--color-grey); color: var(--color-grey);
box-shadow: 0 0 0 1px inset var(--color-grey); }
}
@media (min-width: 769px) {
.navbar__list-item--secondary {
.navbar__link-wrapper {
border: 1px solid var(--color-grey);
}
} }
} }