Merge branch 'master' of gitlab.cip.ifi.lmu.de:jost/UniWorX into feat/multi-file-field

This commit is contained in:
Gregor Kleen 2018-04-03 14:53:18 +02:00
commit 9a26d17c5e
32 changed files with 872 additions and 370 deletions

View File

@ -18,11 +18,10 @@
- i18n der
Links -> MenuItems verwenden wie bisher
Page Titles -> setTitleI
Buttons?
Buttons? -> Kann leicht geändert werden!
Was ist mit einfachen Text Feldern, z.B. die Beschriftung von Knöpfen wie in Handler.Course.getCourseListTermR, Zeile 66 "pageActions" für menuItemLabel?
** Page pageActions
- Berechtigungen prüfen?
** Page pageActions - Berechtigungen prüfen?
=> Eigener Constructor statt NavbarLeft/Right?!

View File

@ -87,19 +87,17 @@ main = db $ do
, courseTermId = TermKey summer2018
, courseSchoolId = ifi
, courseCapacity = Just 20
, courseCreated = now
, courseChanged = now
, courseCreatedBy = gkleen
, courseChangedBy = gkleen
, courseHasRegistration = True
, courseRegisterFrom = Just now
, courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now )
}
insert_ $ CourseEdit jost now ffp
void . insert $ DegreeCourse ifiBsc ffp
void . insert $ DegreeCourse ifiMsc ffp
void . insert $ Lecturer gkleen ffp
void . insert $ Corrector gkleen ffp (ByProportion 1)
void . insert $ Sheet ffp "Blatt 1" Nothing NotGraded Nothing now now Nothing Nothing now now gkleen gkleen
insert_ $ Corrector gkleen ffp (ByProportion 1)
sheetkey <- insert $ Sheet ffp "Blatt 1" Nothing NotGraded NoGroups Nothing Nothing now now Nothing Nothing
insert_ $ SheetEdit gkleen now sheetkey
-- EIP
eip <- insert Course
{ courseName = "Einführung in die Programmierung"
@ -109,14 +107,11 @@ main = db $ do
, courseTermId = TermKey summer2017
, courseSchoolId = ifi
, courseCapacity = Just 20
, courseCreated = now
, courseChanged = now
, courseCreatedBy = fhamann
, courseChangedBy = fhamann
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
insert_ $ CourseEdit fhamann now eip
void . insert $ DegreeCourse ifiBsc eip
void . insert $ DegreeCourse ifiMsc eip
void . insert $ Lecturer fhamann eip
@ -129,14 +124,11 @@ main = db $ do
, courseTermId = TermKey summer2018
, courseSchoolId = ifi
, courseCapacity = Just 20
, courseCreated = now
, courseChanged = now
, courseCreatedBy = fhamann
, courseChangedBy = fhamann
, courseHasRegistration = True
, courseRegisterFrom = Just now
, courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now )
}
insert_ $ CourseEdit fhamann now ixd
void . insert $ DegreeCourse ifiBsc ixd
void . insert $ Lecturer fhamann ixd
-- concept development
@ -148,14 +140,11 @@ main = db $ do
, courseTermId = TermKey winter2017
, courseSchoolId = ifi
, courseCapacity = Just 30
, courseCreated = now
, courseChanged = now
, courseCreatedBy = fhamann
, courseChangedBy = fhamann
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
insert_ $ CourseEdit fhamann now ux3
void . insert $ DegreeCourse ifiBsc ux3
void . insert $ Lecturer fhamann ux3
-- promo
@ -167,14 +156,11 @@ main = db $ do
, courseTermId = TermKey summer2017
, courseSchoolId = ifi
, courseCapacity = Just 50
, courseCreated = now
, courseChanged = now
, courseCreatedBy = jost
, courseChangedBy = jost
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
insert_ $ CourseEdit jost now pmo
void . insert $ DegreeCourse ifiBsc pmo
void . insert $ Lecturer jost pmo
-- datenbanksysteme
@ -186,13 +172,11 @@ main = db $ do
, courseTermId = TermKey summer2018
, courseSchoolId = ifi
, courseCapacity = Just 50
, courseCreated = now
, courseChanged = now
, courseCreatedBy = jost
, courseChangedBy = jost
, courseHasRegistration = False
, courseRegisterFrom = Nothing
, courseRegisterTo = Nothing
}
insert_ $ CourseEdit gkleen now dbs
void . insert $ DegreeCourse ifiBsc dbs
void . insert $ Lecturer jost dbs
void . insert $ Lecturer gkleen dbs
void . insert $ Lecturer jost dbs

47
models
View File

@ -61,14 +61,14 @@ Course
termId TermId
schoolId SchoolId
capacity Int Maybe
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo
registerFrom UTCTime Maybe
registerTo UTCTime Maybe
CourseTermShort termId shorthand
CourseEdit
user UserId
time UTCTime
course CourseId
Lecturer
userId UserId
courseId CourseId
@ -103,15 +103,11 @@ Sheet
activeTo UTCTime
hintFrom UTCTime Maybe
solutionFrom UTCTime Maybe
created UTCTime -- delete
changed UTCTime -- delete
createdBy UserId -- delete
changedBy UserId -- delete
CourseSheet courseId name
SheetEdit
sheet SheetId
user UserId
time UTCTime
sheet SheetId
SheetFile
sheetId SheetId
fileId FileId
@ -128,11 +124,11 @@ Submission
ratingComment Text Maybe
ratingBy UserId Maybe
ratingTime UTCTime Maybe
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
deriving Show
SubmissionEdit
user UserId
time UTCTime
submission SubmissionId
SubmissionFile
submissionId SubmissionId
fileId FileId
@ -147,10 +143,10 @@ SubmissionUser
SubmissionGroup
courseId CourseId
name Text
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
SubmissionGroupEdit
user UserId
time UTCTime
submissionGroup SubmissionGroupId
SubmissionGroupUser
submissionGroupId SubmissionGroupId
userId UserId
@ -169,13 +165,12 @@ Booking
end UTCTime
weekly Bool
exceptions [Day] -- only if weekly, begin in exception
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
bookedFor RoomForId
room RoomId
BookingEdit
user UserId
time UTCTime
boooking BookingId
Room
name Text
capacity Int Maybe
@ -201,10 +196,10 @@ Exam
deregistrationEnd UTCTime
ratingVisible Bool
statisticsVisible Bool
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
ExamEdit
user UserId
time UTCTime
exam ExamId
ExamUser
userId UserId
examId ExamId

39
routes
View File

@ -6,36 +6,29 @@
/ HomeR GET POST
/profile ProfileR GET
/users UsersR GET
/users UsersR GET !adminAny
/term TermShowR GET
/term/edit TermEditR GET POST
/term/#TermId/edit TermEditExistR GET
/term TermShowR GET
/term/edit TermEditR GET POST !adminAny
/term/#TermId/edit TermEditExistR GET !adminAny
/course/ CourseListR GET
!/course/new CourseNewR GET POST
!/course/new CourseNewR GET POST !lecturerAny
!/course/#TermId CourseListTermR GET
/course/#TermId/#Text CourseR:
/show CourseShowR GET POST
/edit CourseEditR GET POST !lecturer
-- /course/#TermId/#Text CourseR !tag:
-- /edit CourseEditR GET POST
-- /show CourseShowR GET POST -- CourseR tid csh CourseShowR
-- /ex/#Text SheetR: !registered
-- /show
-- /edit -- CourseR tid csg (SheetR csh SheetEditR)
-- /delete
/course/#TermId/#Text/edit CourseEditR GET
/course/#TermId/#Text/show CourseShowR GET POST
/ex SheetR !registered:
/ SheetListR GET
/#Text/show SheetShowR GET !time
/#Text/#SheetFileType/#FilePath SheetFileR GET !time
/new SheetNewR GET POST !lecturer
/#Text/edit SheetEditR GET POST !lecturer
/#Text/delete SheetDelR GET POST !lecturer
/course/#TermId/#Text/ex/ SheetListR GET
/course/#TermId/#Text/ex/#Text/show SheetShowR GET
/course/#TermId/#Text/ex/#Text/#SheetFileType/#FilePath SheetFileR GET
/course/#TermId/#Text/ex/new SheetNewR GET POST
/course/#TermId/#Text/ex/#Text/edit SheetEditR GET POST
/course/#TermId/#Text/ex/#Text/delete SheetDelR GET POST
-- TODO below
/submission SubmissionListR GET POST
/submission/#CryptoUUIDSubmission SubmissionR GET POST
/submissions.zip SubmissionDownloadMultiArchiveR POST

View File

@ -4,6 +4,7 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NamedFieldPuns #-}
@ -53,6 +54,9 @@ import System.FilePath
import Handler.Utils.Templates
-- infixl 9 :$:
-- pattern a :$: b = a b
-- | The foundation datatype for your application. This can be a good place to
-- keep settings and values requiring initialization before your application
-- starts running, such as database connections. Every handler will have
@ -80,7 +84,9 @@ data UniWorX = UniWorX
-- type Widget = WidgetT UniWorX IO ()
mkYesodData "UniWorX" $(parseRoutesFile "routes")
type DB a = YesodDB UniWorX a
-- Pattern Synonyms for convenience
pattern CSheetR tid csh ptn = CourseR tid csh (SheetR ptn)
data MenuItem = MenuItem
{ menuItemLabel :: Text
@ -89,13 +95,16 @@ data MenuItem = MenuItem
, menuItemAccessCallback :: Handler Bool
}
data MenuTypes
= NavbarAside { menuItem :: MenuItem }
| NavbarRight { menuItem :: MenuItem }
| NavbarExtra { menuItem :: MenuItem }
| NavbarSecondary { menuItem :: MenuItem }
data MenuTypes -- Semantische Rolle:
= NavbarAside { menuItem :: MenuItem } -- TODO
| NavbarExtra { menuItem :: MenuItem } -- TODO
| NavbarRight { menuItem :: MenuItem } -- Generell, nahezu immer sichtbar
| NavbarSecondary { menuItem :: MenuItem } -- Generell, nahezu immer sichtbar
| PageActionPrime { menuItem :: MenuItem } -- Seitenspezifische Aktion, häufig
| PageActionSecondary { menuItem :: MenuItem } -- Seitenspezifische Aktion, selten
-- | A convenient synonym for creating forms.
-- | Convenient Type Synonyms:
type DB a = YesodDB UniWorX a
type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget)
mkMessage "UniWorX" "messages" "de"
@ -151,7 +160,7 @@ instance Yesod UniWorX where
isAuthorized TermShowR _ = return Authorized
isAuthorized CourseListR _ = return Authorized
isAuthorized (CourseListTermR _) _ = return Authorized
isAuthorized (CourseShowR _ _) _ = return Authorized
isAuthorized (CourseR _ _ CourseShowR) _ = return Authorized
isAuthorized (CryptoUUIDDispatchR _) _ = return Authorized
isAuthorized SubmissionListR _ = isAuthenticated
isAuthorized SubmissionDownloadMultiArchiveR _ = isAuthenticated
@ -198,6 +207,12 @@ instance Yesod UniWorX where
makeLogger = return . appLogger
isAuthorizedDB :: Route UniWorX -> Bool -> YesodDB UniWorX AuthResult
isAuthorizedDB route@(routeAttrs -> attrs) writeable
| "adminAny" `member` attrs = adminAccess Nothing
| "lecturerAny" `member` attrs = lecturerAccess Nothing
isAuthorizedDB UsersR _ = adminAccess Nothing
isAuthorizedDB (SubmissionR cID) _ = submissionAccess $ Right cID
isAuthorizedDB (SubmissionDownloadSingleR cID _) _ = submissionAccess $ Right cID
@ -205,14 +220,14 @@ isAuthorizedDB (SubmissionDownloadArchiveR (splitExtension -> (baseName, _))) _
isAuthorizedDB TermEditR _ = adminAccess Nothing
isAuthorizedDB (TermEditExistR _) _ = adminAccess Nothing
isAuthorizedDB CourseNewR _ = lecturerAccess Nothing
isAuthorizedDB (CourseEditR t c) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (SheetListR t c) False = return Authorized --
isAuthorizedDB (SheetShowR t c s) _ = return Authorized -- TODO: nur für angemeldete Kursteilnehmer falls sichtbar, sonst nur Lectrurer oder Korrektor
isAuthorizedDB (SheetFileR t c s _ _) _ = return Authorized -- TODO: nur für angemeldete Kursteilnehmer falls sichtbar, sonst nur Lectrurer oder Korrektor
isAuthorizedDB (SheetListR t c) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (SheetNewR t c) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (SheetEditR t c s) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (SheetDelR t c s) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c CourseEditR) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR SheetListR)) False = return Authorized --
isAuthorizedDB (CourseR t c (SheetR SheetListR)) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR (SheetShowR s))) _ = return Authorized -- TODO: nur für angemeldete Kursteilnehmer falls sichtbar, sonst nur Lectrurer oder Korrektor
isAuthorizedDB (CourseR t c (SheetR (SheetFileR s _ _))) _ = return Authorized -- TODO: nur für angemeldete Kursteilnehmer falls sichtbar, sonst nur Lectrurer oder Korrektor
isAuthorizedDB (CourseR t c (SheetR SheetNewR)) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR (SheetEditR s))) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseR t c (SheetR (SheetDelR s))) _ = courseLecturerAccess . entityKey =<< getBy404 (CourseTermShort t c)
isAuthorizedDB (CourseEditIDR cID) _ = do
courseId <- decrypt cID
courseLecturerAccess courseId
@ -285,15 +300,15 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb CourseListR = return ("Kurs", Just HomeR)
breadcrumb (CourseListTermR term) = return (toPathPiece term, Just TermShowR)
breadcrumb (CourseShowR term course) = return (course, Just $ CourseListTermR term)
breadcrumb (CourseR term course CourseShowR) = return (course, Just $ CourseListTermR term)
breadcrumb CourseNewR = return ("Neu", Just CourseListR)
breadcrumb (CourseEditR _ _) = return ("Editieren", Just CourseListR)
breadcrumb (CourseR _ _ CourseEditR) = return ("Editieren", Just CourseListR)
breadcrumb (SheetListR tid csh) = return ("Übungen",Just $ CourseShowR tid csh)
breadcrumb (SheetNewR tid csh) = return ("Neu", Just $ SheetListR tid csh)
breadcrumb (SheetShowR tid csh shn) = return (shn, Just $ SheetListR tid csh)
breadcrumb (SheetEditR tid csh shn) = return ("Edit", Just $ SheetShowR tid csh shn)
breadcrumb (SheetDelR tid csh shn) = return ("DELETE", Just $ SheetShowR tid csh shn)
breadcrumb (CourseR tid csh (SheetR SheetListR)) = return ("Übungen",Just $ CourseR tid csh CourseShowR)
breadcrumb (CourseR tid csh (SheetR SheetNewR )) = return ("Neu", Just $ CourseR tid csh $ SheetR SheetListR)
breadcrumb (CourseR tid csh (SheetR (SheetShowR shn))) = return (shn, Just $ CourseR tid csh $ SheetR SheetListR)
breadcrumb (CourseR tid csh (SheetR (SheetEditR shn))) = return ("Edit", Just $ CourseR tid csh $ SheetR $ SheetShowR shn)
breadcrumb (CourseR tid csh (SheetR (SheetDelR shn))) = return ("DELETE", Just $ CourseR tid csh $ SheetR $ SheetShowR shn)
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR)
@ -384,6 +399,14 @@ defaultMenuLayout menu widget = do
asidenav = $(widgetFile "widgets/asidenav")
breadcrumbs :: Widget
breadcrumbs = $(widgetFile "widgets/breadcrumbs")
pageactionprime :: Widget
pageactionprime = $(widgetFile "widgets/pageactionprime")
-- functions to determine if there are page-actions
isPageActionPrime :: MenuTypes -> Bool
isPageActionPrime (PageActionPrime _) = True
isPageActionPrime _ = False
hasPageActions :: Bool
hasPageActions = any isPageActionPrime menuTypes
pc <- widgetToPageContent $ do
addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900"
@ -391,6 +414,7 @@ defaultMenuLayout menu widget = do
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_icons_css
$(widgetFile "default-layout")
$(widgetFile "standalone/modal")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/sortable")
$(widgetFile "standalone/inputs")

View File

@ -40,7 +40,7 @@ getCourseListTermR tidini = do
let c = entityVal ckv
shd = courseShorthand c
tid = courseTermId c
in [whamlet| <a href=@{CourseShowR tid shd}>#{shd} |] )
in [whamlet| <a href=@{CourseR tid shd CourseShowR}>#{shd} |] )
-- , headed "Institut" $ [shamlet| #{course} |]
, headed "Beginn Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterFrom.entityVal
, headed "Ende Anmeldung" $ fromString.(maybe "" formatTimeGerWD).courseRegisterTo.entityVal
@ -54,17 +54,17 @@ getCourseListTermR tidini = do
shd = courseShorthand c
tid = courseTermId c
in do
adminLink <- handlerToWidget $ isAuthorized (CourseEditR tid shd ) False
adminLink <- handlerToWidget $ isAuthorized (CourseR tid shd CourseEditR) False
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditR tid shd) else ""
[whamlet|
$if adminLink == Authorized
<a href=@{CourseEditR tid shd}>
<a href=@{CourseR tid shd CourseEditR}>
editieren
|]
)
]
let pageLinks =
[ NavbarAside $ MenuItem
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neuer Kurs"
, menuItemIcon = Just "book"
, menuItemRoute = CourseNewR
@ -93,11 +93,11 @@ getCourseShowR tid csh = do
let course = entityVal courseEnt
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
let pageActions =
[ NavbarAside $ MenuItem
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Übungsblätter"
, menuItemIcon = Nothing
, menuItemRoute = SheetListR tid csh
, menuItemAccessCallback = (== Authorized) <$> isAuthorized (SheetListR tid csh) False
, menuItemRoute = CSheetR tid csh SheetListR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized (CSheetR tid csh SheetListR) False
}
]
defaultLinkLayout pageActions $ do
@ -145,6 +145,9 @@ getCourseEditR tid csh = do
course <- runDB $ getBy $ CourseTermShort tid csh
courseEditHandler course
postCourseEditR :: TermId -> Text -> Handler Html
postCourseEditR = getCourseEditR
getCourseEditIDR :: CryptoUUIDCourse -> Handler Html
getCourseEditIDR cID = do
cIDKey <- getsYesod appCryptoIDKey
@ -174,7 +177,7 @@ courseEditHandler course = do
, cfTerm = tid
})) -> do -- create new course
let tident = unTermKey tid
actTime <- liftIO getCurrentTime
now <- liftIO getCurrentTime
insertOkay <- runDB $ insertUnique $ Course
{ courseName = cfName res
, courseDescription = cfDesc res
@ -186,14 +189,12 @@ courseEditHandler course = do
, courseHasRegistration = cfHasReg res
, courseRegisterFrom = cfRegFrom res
, courseRegisterTo = cfRegTo res
, courseCreated = actTime
, courseChanged = actTime
, courseCreatedBy = aid
, courseChangedBy = aid
}
}
case insertOkay of
(Just cid) -> do
runDB $ insert_ $ Lecturer aid cid
runDB $ do
insert_ $ CourseEdit aid now cid
insert_ $ Lecturer aid cid
addMessageI "info" $ MsgCourseNewOk tident csh
redirect $ CourseListTermR tid
Nothing ->
@ -205,7 +206,7 @@ courseEditHandler course = do
, cfTerm = tid
})) -> do -- edit existing course
let tident = unTermKey tid
actTime <- liftIO getCurrentTime
now <- liftIO getCurrentTime
-- addMessage "debug" [shamlet| #{show res}|]
runDB $ do
old <- get cid
@ -228,9 +229,9 @@ courseEditHandler course = do
-- , CourseRegisterFrom =. cfRegFrom res
-- , CourseRegisterTo =. cfRegTo res
-- , CourseChangedBy =. aid
-- , CourseChanged =. actTime
-- , CourseChanged =. now
-- ]
updOkay <- replace cid ( -- TODO replaceUnique requires Eq?!
_updOkay <- replace cid ( -- TODO replaceUnique requires Eq?!
Course { courseName = cfName res
, courseDescription = cfDesc res
, courseLinkExternal = cfLink res
@ -238,15 +239,12 @@ courseEditHandler course = do
, courseTermId = cfTerm res
, courseSchoolId = cfSchool res
, courseCapacity = cfCapacity res
, courseChanged = actTime
, courseChangedBy = aid
, courseCreated = courseCreated oldCourse
, courseCreatedBy = courseCreatedBy oldCourse
, courseHasRegistration = cfHasReg res
, courseRegisterFrom = cfRegFrom res
, courseRegisterTo = cfRegTo res
}
)
insert_ $ CourseEdit aid now cid
-- if (isNothing updOkay)
-- then do
addMessageI "info" $ MsgCourseEditOk tident csh
@ -256,7 +254,7 @@ courseEditHandler course = do
(FormFailure _) -> addMessageI "warning" MsgInvalidInput
other -> addMessage "error" $ [shamlet| Error: #{show other}|]
let formTitle = "Kurs editieren/anlegen" :: Text
let actionUrl = CourseNewR -- CourseEditR -- TODO
actionUrl <- fromMaybe CourseNewR <$> getCurrentRoute
defaultLayout $ do
setTitle [shamlet| #{formTitle} |]
$(widgetFile "formPage")

View File

@ -172,30 +172,30 @@ getSheetList courseEnt = do
rated <- count $ (SubmissionRatingTime !=. Nothing):sheetsub
return (sid, sheet, (submissions, rated))
let colBase = mconcat
[ headed "Blatt" $ \(sid,sheet,_) -> linkButton (toWgt $ sheetName sheet) BCLink $ SheetShowR tid csh (sheetName sheet)
[ headed "Blatt" $ \(sid,sheet,_) -> linkButton (toWgt $ sheetName sheet) BCLink $ CourseR tid csh $ SheetR $ SheetShowR $ sheetName sheet
, headed "Abgabe ab" $ toWgt . formatTimeGerWD . sheetActiveFrom . snd3
, headed "Abgabe bis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3
, headed "Bewertung" $ toWgt . show . sheetType . snd3
, headed "Korrigiert" $ toWgt . snd . trd3
, headed "Eingereicht" $ toWgt . fst . trd3
]
let colAdmin = mconcat -- only show edit button for allowed course assistants
[ headed "" $ \s -> linkButton "Edit" BCLink $ SheetEditR tid csh $ sheetName $ snd3 s
, headed "" $ \s -> linkButton "Delete" BCLink $ SheetDelR tid csh $ sheetName $ snd3 s
[ headed "Korrigiert" $ toWgt . snd . trd3
, headed "Eingereicht" $ toWgt . fst . trd3
, headed "" $ \s -> linkButton "Edit" BCLink $ CourseR tid csh $ SheetR $ SheetEditR $ sheetName $ snd3 s
, headed "" $ \s -> linkButton "Delete" BCLink $ CourseR tid csh $ SheetR $ SheetDelR $ sheetName $ snd3 s
]
showAdmin <- case sheets of
((_,firstSheet,_):_) -> do
setUltDestCurrent
(Authorized ==) <$> isAuthorized (SheetEditR tid csh $ sheetName firstSheet) False
(Authorized ==) <$> isAuthorized (CourseR tid csh $ SheetR $ SheetEditR $ sheetName firstSheet) False
_otherwise -> return False
let colSheets = if showAdmin
then colBase `mappend` colAdmin
else colBase
let pageActions =
[ NavbarAside $ MenuItem
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neues Übungsblatt"
, menuItemIcon = Nothing
, menuItemRoute = SheetNewR tid csh
, menuItemRoute = CSheetR tid csh SheetNewR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseNewR False
}
]
@ -221,7 +221,7 @@ getSheetShowR tid csh shn = do
E.where_ (sheet E.^. SheetId E.==. E.val sid )
-- return desired columns
return $ (file E.^. FileTitle, sheetFile E.^. SheetFileType)
let fileLinks = map (\(E.Value fName, E.Value fType) -> SheetFileR tid csh shn fType fName) fileNameTypes
let fileLinks = map (\(E.Value fName, E.Value fType) -> CSheetR tid csh (SheetFileR shn fType fName)) fileNameTypes
defaultLayout $ do
setTitle $ toHtml $ T.append "Übung " $ sheetName sheet
@ -295,8 +295,6 @@ getSheetEditR tid csh shn = do
}
let action newSheet = do
replaceRes <- myReplaceUnique sid $ newSheet
{ sheetCreated = sheetCreated
, sheetCreatedBy = sheetChangedBy }
case replaceRes of
Nothing -> return $ Just sid
(Just _err) -> return $ Nothing -- More specific error message for edit old sheet could go here
@ -314,8 +312,8 @@ handleSheetEdit tid csh msId template dbAction = do
case res of
(FormSuccess SheetForm{..}) -> do
saveOkay <- runDB $ do
cid <- getKeyBy404 $ CourseTermShort tid csh
actTime <- liftIO getCurrentTime
cid <- getKeyBy404 $ CourseTermShort tid csh
let newSheet = Sheet
{ sheetCourseId = cid
, sheetName = sfName
@ -328,10 +326,6 @@ handleSheetEdit tid csh msId template dbAction = do
, sheetActiveTo = sfActiveTo
, sheetHintFrom = sfHintFrom
, sheetSolutionFrom = sfSolutionFrom
, sheetCreated = actTime -- dbAction adjusts this for replacement, TODO: eigene Tabelle für changedBy
, sheetChanged = actTime
, sheetCreatedBy = aid -- dbAction adjusts this for replacement
, sheetChangedBy = aid
}
mbsid <- dbAction newSheet
case mbsid of
@ -340,16 +334,17 @@ handleSheetEdit tid csh msId template dbAction = do
whenIsJust sfSheetF $ insertSheetFile' sid SheetExercise
whenIsJust sfHintF $ insertSheetFile sid SheetHint
whenIsJust sfSolutionF $ insertSheetFile sid SheetSolution
insert_ $ SheetEdit aid actTime sid
addMessageI "info" $ MsgSheetEditOk tident csh sfName
return True
when saveOkay $ redirect $ SheetShowR tid csh sfName -- redirect must happen outside of runDB
when saveOkay $ redirect $ CSheetR tid csh $ SheetShowR sfName -- redirect must happen outside of runDB
(FormFailure msgs) -> forM_ msgs $ (addMessage "warning") . toHtml
_ -> return ()
let pageTitle = maybe (MsgSheetTitleNew tident csh)
(MsgSheetTitle tident csh) mbshn
let formTitle = pageTitle
let formText = Nothing :: Maybe UniWorXMessage
actionUrl <- fromMaybe (SheetNewR tid csh) <$> getCurrentRoute
actionUrl <- fromMaybe (CSheetR tid csh SheetNewR) <$> getCurrentRoute
defaultLayout $ do
setTitleI pageTitle
$(widgetFile "formPageI18n")
@ -361,19 +356,19 @@ getSheetDelR tid csh shn = do
let tident = unTermKey tid
((result,formWidget), formEnctype) <- runFormPost (buttonForm :: Form BtnDelete)
case result of
(FormSuccess BtnAbort) -> redirectUltDest $ SheetShowR tid csh shn
(FormSuccess BtnAbort) -> redirectUltDest $ CSheetR tid csh $ SheetShowR shn
(FormSuccess BtnDelete) -> do
runDB $ fetchSheetId tid csh shn >>= deleteCascade
-- TODO: deleteCascade löscht aber nicht die hochgeladenen Dateien!!!
setMessageI $ MsgSheetDelOk tident csh shn
redirect $ SheetListR tid csh
redirect $ CSheetR tid csh SheetListR
_other -> do
submissionno <- runDB $ do
sid <- fetchSheetId tid csh shn
count [SubmissionSheetId ==. sid]
let formTitle = MsgSheetDelTitle tident csh shn
let formText = Just $ MsgSheetDelText submissionno
let actionUrl = SheetDelR tid csh shn
let actionUrl = CSheetR tid csh $ SheetDelR shn
defaultLayout $ do
setTitleI $ MsgSheetTitle tident csh shn
$(widgetFile "formPageI18n")

View File

@ -8,6 +8,7 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ViewPatterns #-}
module Handler.Submission where
@ -53,7 +54,7 @@ submissionTable = do
(,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s
let
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseShowR courseTermId courseShorthand
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseR courseTermId courseShorthand CourseShowR
courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName
anchorSubmission (_, cUUID, _) = SubmissionR cUUID
submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID
@ -177,8 +178,12 @@ postSubmissionDownloadMultiArchiveR = do
withinDirectory f@File{..} = f { fileTitle = directoryName </> fileTitle }
lastEditMb <- lift $ selectList [SubmissionEditSubmission ==. submissionID] [Desc SubmissionEditTime, LimitTo 1]
lastEditTime <- case lastEditMb of
[(submissionEditTime.entityVal -> time)] -> return time
_other -> liftIO getCurrentTime
yield $ File
{ fileModified = submissionChanged
{ fileModified = lastEditTime
, fileTitle = directoryName
, fileContent = Nothing
}

View File

@ -76,7 +76,7 @@ getTermShowR = do
, dbtIdent = "terms" :: Text
}
let pageActions =
[ NavbarAside $ MenuItem
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Neues Semester"
, menuItemIcon = Nothing
, menuItemRoute = TermEditR

View File

@ -70,12 +70,9 @@ sinkSubmission sheetId userId mExists = do
submissionRatingComment = Nothing
submissionRatingBy = Nothing
submissionRatingTime = Nothing
submissionCreated = now
submissionChanged = now
submissionCreatedBy = userId
submissionChangedBy = userId
(sId, isUpdate) <- lift $ maybe ((, False) <$> insert Submission{..}) return mExists
(sId, isUpdate) <- lift $ maybe ((, False) <$> (insert Submission{..} >>= (\sid -> sid <$ insert (SubmissionEdit userId now sid)))) return mExists
sId <$ sinkSubmission' sId isUpdate
where
@ -184,9 +181,9 @@ sinkSubmission sheetId userId mExists = do
alreadyTouched <- gets $ getAny . sinkSubmissionTouched
when (not alreadyTouched) $ do
now <- liftIO getCurrentTime
lift . update submissionId $ case isUpdate of
False -> [ SubmissionChangedBy =. userId, SubmissionChanged =. now ]
True -> [ SubmissionRatingBy =. Just userId, SubmissionRatingTime =. Just now ]
lift $ case isUpdate of
False -> insert_ $ SubmissionEdit userId now submissionId
True -> update submissionId [ SubmissionRatingBy =. Just userId, SubmissionRatingTime =. Just now ]
tell $ mempty{ sinkSubmissionTouched = Any True }
finalize :: SubmissionSinkState -> YesodDB UniWorX ()

View File

@ -6,3 +6,17 @@ import Import.NoFoundation
lipsum :: WidgetT site IO ()
lipsum = $(widgetFile "widgets/lipsum")
modal :: [Char] -> Maybe [Char] -> WidgetT site IO ()
modal modalTrigger (Just modalContent) = do
let
modalId :: Int32
modalId = 13
$(widgetFile "widgets/modal")
modal modalTrigger Nothing = do
let
modalId :: Int32
modalId = 13
modalContent :: [Char]
modalContent = "placeholder"
$(widgetFile "widgets/modal")

View File

@ -55,7 +55,7 @@ deriveJSON defaultOptions ''SheetType
derivePersistFieldJSON "SheetType"
data SheetGroup
= Arbitrary { maxParticipants :: Int }
= Arbitrary { maxParticipants :: Int } -- Distinguish Limited/Arbitrary
| RegisteredGroups
| NoGroups
deriving (Show, Read, Eq)

View File

@ -16,7 +16,7 @@
\ bis #{formatTimeGerWD regTo}
<div>
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}>
<form method=post action=@{CourseR tid csh CourseShowR} enctype=#{regEnctype}>
^{regWidget}
<div .course-header__title>

View File

@ -38,7 +38,12 @@ $newline never
\ });
}
<body>
<body .no-js>
<!-- removes no-js class from body if client supports javascript -->
<script>
document.body.classList.remove('no-js');
^{pageBody pc}
$maybe analytics <- appAnalytics $ appSettings master

View File

@ -13,5 +13,8 @@
$with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}">#{msg}
<!-- prime page actions -->
^{pageactionprime}
<!-- actual content -->
^{widget}

View File

@ -25,6 +25,7 @@
--lighterbase: #5F98C2;
--whitebase: #FCFFFA;
--greybase: #B1B5C0;
--lightgreybase: #D9DEDB;
--blackbase: #1A2A36;
--fontbase: #34303a;
--fontsec: #5b5861;
@ -158,7 +159,7 @@ th {
input[type="submit"],
input[type="button"],
button,
.btn {
.btn, a.btn {
outline: 0;
border: 0;
box-shadow: 0;
@ -174,12 +175,14 @@ button,
}
input.btn-primary,
button.btn-primary,
a.btn.btn-primary,
.btn.btn-primary {
background-color: var(--primarybase);
}
input.btn-info,
button.btn-info,
a.btn.btn-info,
.btn.btn-info {
background-color: var(--infobase)
}
@ -187,6 +190,7 @@ button.btn-info,
input[type="submit"][disabled],
input[type="button"][disabled],
button[disabled],
a.btn[disabled],
.btn[disabled] {
opacity: 0.3;
background-color: var(--greybase);
@ -196,14 +200,17 @@ button[disabled],
input[type="submit"]:not([disabled]):hover,
input[type="button"]:not([disabled]):hover,
button:not([disabled]):hover,
a.btn:not([disabled]):hover,
.btn:not([disabled]):hover {
background-color: var(--lighterbase);
text-decoration: underline;
color: white;
}
input[type="submit"].btn-info:hover,
input[type="button"].btn-info:hover,
button.btn-info:hover,
a.btn.btn-info:hover,
.btn.btn-info:hover {
background-color: var(--greybase)
}

View File

@ -39,7 +39,7 @@
<tbody>
<tr>
<td>0
<td>14
<td>NT2
<td>CON2
<td>3
<tr>
@ -57,6 +57,11 @@
<td>43
<td>T2C2
<td>35
<tr>
<td>4
<td>73
<td>CA62
<td>7
<hr>
<div .container>
@ -67,3 +72,29 @@
Knopf-Test:
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
^{btnWdgt}
<li><br>
Modals:
^{modal ".toggler1" Nothing}
<a href="/" .btn.toggler1>Klick mich für Ajax-Test
<noscript>(Für Modals bitte JS aktivieren)</noscript>
^{modal ".toggler2" (Just "Test wegen Modal")}
<div .btn.toggler2>Klick mich für Content-Test
<noscript>(Für Modals bitte JS aktivieren)</noscript>
<li><br>
Multi-File-Input für bereits hochgeladene Dateien:
<form>
<div .form-group>
<label .form-group__label>Datei(en)
$# file 1
<div .file-checkbox__container>
<label .file-checkbox__label.reactive-label.btn for="f2-1">Datenschutz.txt
<div .checkbox>
<input .file-checkbox id="f2-1" name="f2" value="Datenschutz.txt" type="checkbox">
<label for="f2-1">
$# file 2
<div .file-checkbox__container>
<label .file-checkbox__label.reactive-label.btn for="f2-2">fill-db.hs
<div .checkbox>
<input .file-checkbox id="f2-2" name="f2" value="fill-db.hs" type="checkbox">
<label for="f2-2">

View File

@ -29,7 +29,7 @@
$forall fileLink <- fileLinks
<li>
$case fileLink
$of SheetFileR _ _ _ typ name
$of CourseR _ _ (SheetR (SheetFileR _ typ name))
#{toPathPiece typ}
<a href=@{fileLink}>#{name}
$of other

View File

@ -24,10 +24,12 @@
});
};
// allows for multiple file uploads with separate inputs
window.utils.reactiveFileUpload = function(input, parent) {
var currValidInputCount = 0;
var addMore = false;
var inputName = input.getAttribute('name');
var isMulti = input.getAttribute('multiple') ? true : false;
// FileInput PseudoClass
function FileInput(container, input, label, remover) {
this.container = container;
@ -42,7 +44,7 @@
this.remove = function() {
this.container.remove();
}
this.isValid = function() {
this.wasValid = function() {
return this.container.classList.contains('file-input__container--valid');
}
}
@ -61,7 +63,9 @@
parent.classList.add('form-group--valid')
}
submitBtn.removeAttribute('disabled');
addNextInput();
if (isMulti) {
addNextInput();
}
} else {
if (parent.classList.contains('form-group')) {
parent.classList.remove('form-group--valid')
@ -78,14 +82,16 @@
var fileName = filePath[filePath.length - 1];
fileInput.label.innerHTML = fileName;
// increase count if this field was empty previously
if (!fileInput.isValid()) {
if (!fileInput.wasValid()) {
currValidInputCount++;
}
fileInput.container.classList.add('file-input__container--valid')
// show next input
} else {
currValidInputCount--;
fileInput.remove();
if (isMulti) {
currValidInputCount--;
}
clearInput(fileInput);
}
updateForm();
});
@ -95,26 +101,36 @@
fileInput.input.addEventListener('blur', function() {
fileInput.container.classList.remove('pseudo-focus');
});
fileInput.label.addEventListener('click', function() {
fileInput.input.click();
});
fileInput.remover.addEventListener('click', function() {
if (fileInput.isValid()) {
if (fileInput.wasValid()) {
currValidInputCount--;
}
fileInput.remove();
updateForm();
clearInput(fileInput);
});
}
// clears or removes fileinput based on multi-file or not
function clearInput(fileInput) {
if (isMulti) {
fileInput.remove();
} else {
fileInput.container.classList.remove('file-input__container--valid')
fileInput.label.innerHTML = '';
}
updateForm();
}
// create new wrapped input element with name name
function makeInput(name) {
var cont = document.createElement('div');
var desc = document.createElement('span');
var desc = document.createElement('label');
var nextInput = document.createElement('input');
var remover = document.createElement('div');
cont.classList.add('file-input__container');
desc.classList.add('file-input__label', 'btn');
nextInput.classList.add('js-file-input');
desc.setAttribute('for', name + '-' + currValidInputCount);
remover.classList.add('file-input__remover');
nextInput.setAttribute('id', name + '-' + currValidInputCount);
nextInput.setAttribute('name', name);
nextInput.setAttribute('type', 'file');
cont.appendChild(nextInput);
@ -132,6 +148,39 @@
setup();
}
// to remove previously uploaded files
window.utils.reactiveFileCheckbox = function(input, label, parent) {
// adds eventlistener(s)
function addListener(container) {
container.addEventListener('click', function() {
input.click();
});
input.addEventListener('change', function(event) {
container.classList.toggle('file-checkbox__container--valid', this.checked);
});
}
// initial setup
function setup() {
var cont = input.parentNode;
while (cont !== document.body) {
if (cont.classList.contains('file-checkbox__container')) {
break;
}
cont = cont.parentNode;
}
// take care of properly moving elements
if (input.parentNode.classList.contains('checkbox')) {
input.parentNode.classList.add('file-checkbox__checkbox');
} else {
input.classList.add('file-checkbox__checkbox');
}
addListener(cont);
}
setup();
}
window.utils.reactiveFormGroup = function(formGroup, input) {
// updates to dom
if (input.value.length > 0) {
@ -156,18 +205,28 @@ document.addEventListener('DOMContentLoaded', function() {
// setup reactive labels
Array.from(document.querySelectorAll('.reactive-label')).forEach(function(label) {
var input = document.querySelector('#' + label.getAttribute('for'));
if (!input) {
console.error('No input found for ReactiveLabel! Targeted input: \'#%s\'', label.getAttribute('for'));
return false;
}
var parent = label.parentElement;
var type = input.getAttribute('type');
var isFileInput = /file/i.test(type);
var isFileUpload = /file/i.test(type);
var isFileCheckbox = input.classList.contains('file-checkbox');
var isListening = !RegExp(['date', 'checkbox', 'radio', 'hidden', 'file'].join('|')).test(type);
var isInFormGroup = parent.classList.contains('form-group') && parent.classList.contains('form-group--required');
if (isInFormGroup) {
window.utils.reactiveFormGroup(parent, input);
}
if (isFileInput) {
if (isFileUpload) {
window.utils.reactiveFileUpload(input, parent);
}
if (isFileCheckbox) {
window.utils.reactiveFileCheckbox(input, label, parent);
}
if (isListening) {
window.utils.reactiveInputLabel(input, label);
} else {

View File

@ -3,6 +3,53 @@ form {
margin: 20px 0;
}
/* FORM GROUPS */
.form-group {
position: relative;
display: flex;
display: grid;
grid-template-columns: 25% max-content;
grid-auto-columns: 25%;
grid-gap: 5px;
justify-content: flex-start;
align-items: center;
margin: 10px 0;
padding-left: 10px;
border-left: 8px solid transparent;
}
.form-group--required {
border-left: 8px solid var(--lighterbase);
}
.form-group--valid {
border-left: 8px solid var(--validbase);
}
.form-group--has-error {
border-left: 8px solid var(--errorbase) !important;
}
.form-group__label {
width: 25%;
white-space: nowrap;
font-weight: 600;
}
@media (max-width: 999px) {
.form-group {
grid-template-columns: 1fr;
grid-template-rows: 30px;
align-items: baseline;
margin-top: 17px;
flex-direction: column;
> * {
width: 100%;
}
}
}
/* TEXT INPUTS */
input[type="text"],
input[type="password"],
@ -18,7 +65,7 @@ input[type="email"] {
color: var(--fontbase);
transition: all .1s;
font-size: 16px;
min-width: 300px;
min-width: 400px;
}
input[type="text"]:focus,
@ -37,7 +84,7 @@ textarea {
outline: 0;
border: 0;
padding: 7px 4px;
min-width: 300px;
min-width: 400px;
min-height: 100px;
font-family: var(--fontfamilybase);
font-size: 16px;
@ -51,30 +98,6 @@ textarea:focus {
background-color: transparent;
border-bottom-color: var(--lightbase);
}
/* FORM GROUPS */
.form-group {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(150px, max-content));
grid-auto-columns: minmax(150px, max-content);
grid-gap: 5px;
align-items: center;
margin: 10px 0;
padding-left: 10px;
border-left: 8px solid transparent;
}
.form-group--required {
border-left: 8px solid var(--lighterbase);
}
.form-group--valid {
border-left: 8px solid var(--validbase);
}
.form-group--has-error {
border-left: 8px solid var(--errorbase) !important;
}
/* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */
input[type="checkbox"] {
@ -115,7 +138,6 @@ input[type="checkbox"]:checked::after {
.checkbox,
.radio {
position: relative;
margin: 3px;
> [type="checkbox"],
> [type="radio"] {
@ -124,8 +146,9 @@ input[type="checkbox"]:checked::after {
> label {
display: block;
padding: 7px 13px 7px 30px;
background-color: var(--darkbase);
height: 30px;
width: 30px;
background-color: var(--greybase);
border-radius: 4px;
color: white;
cursor: pointer;
@ -135,8 +158,8 @@ input[type="checkbox"]:checked::after {
> label::after {
content: '';
position: absolute;
top: 15px;
left: 4px;
top: 14px;
left: 5px;
display: block;
width: 20px;
height: 20px;
@ -147,13 +170,13 @@ input[type="checkbox"]:checked::after {
> label::before {
width: 20px;
height: 2px;
transform: scale(0.8, 0.1);
transform: scale(0.1, 0.1);
}
> label::after {
width: 20px;
height: 2px;
transform: scale(0.8, 0.1);
transform: scale(0.1, 0.1);
}
> :checked + label {
@ -161,12 +184,27 @@ input[type="checkbox"]:checked::after {
text-decoration: underline;
}
&:hover > label::before,
&:hover > label {
background-color: var(--lighterbase);
}
&:hover > label::before {
transform: scale(0.8, 0.4);
}
> :checked + label::before {
transform: scale(1, 1) rotate(45deg);
}
&:hover > label::after,
> :checked + label:hover::after,
> :checked + label:hover::before {
transform: scale(1, 1) rotate(0deg);
}
&:hover > label::after {
transform: scale(0.8, 0.4);
}
> :checked + label::after {
transform: scale(1, 1) rotate(-45deg);
}
@ -202,12 +240,6 @@ input[type="checkbox"]:checked::after {
color: var(--fontbase);
}
@media (max-width: 999px) {
.form-group {
grid-template-rows: 30px;
grid-template-columns: 1fr;
align-items: baseline;
margin-top: 17px;
}
.reactive-label {
position: relative;
transform: translate(2px, 30px);
@ -220,7 +252,7 @@ input[type="checkbox"]:checked::after {
}
/* CUSTOM FILE INPUT */
input[type="file"] {
input[type="file"].js-file-input {
color: white;
width: 0.1px;
height: 0.1px;
@ -231,25 +263,34 @@ input[type="file"] {
outline: 0;
border: 0;
}
.file-input__container {
.file-input__container,
.file-checkbox__container {
grid-column-start: 2;
display: flex;
justify-content: space-between;
}
.file-input__label,
.file-input__remover {
.file-input__remover,
.file-checkbox__label,
.file-checkbox__remover {
display: block;
border-radius: 2px;
padding: 5px 13px;
color: var(--whitebase);
cursor: pointer;
}
.file-input__label {
.file-input__label,
.file-checkbox__label {
text-align: left;
position: relative;
height: 30px;
}
.file-input__label.btn {
.file-checkbox__label {
background-color: var(--greybase);
text-decoration: line-through;
}
.file-input__label.btn,
.file-checkbox__label.btn {
padding: 5px 13px;
}
.file-input__label::after,
@ -271,6 +312,9 @@ input[type="file"] {
.file-input__label::before {
transform: translate(-50%, -50%);
}
.file-checkbox__checkbox {
margin-left: 10px;
}
.file-input__remover {
display: none;
width: 40px;
@ -292,6 +336,15 @@ input[type="file"] {
.file-input__container--valid > .file-input__label {
background-color: var(--lightbase);
}
.file-checkbox__container--valid > .file-checkbox__label {
text-decoration: none;
background-color: var(--lighterbase);
&:hover {
background-color: var(--greybase);
text-decoration: line-through;
}
}
.file-input__container--valid > .file-input__label::before,
.file-input__container--valid > .file-input__label::after {
content: none;
@ -300,7 +353,8 @@ input[type="file"] {
display: block;
}
@media (max-width: 999px) {
.file-input__container {
.file-input__container,
.file-checkbox__container {
grid-column-start: 1;
}
}

View File

@ -0,0 +1 @@
<!-- only here to be able to include modal using `toWidget` -->

View File

@ -0,0 +1,100 @@
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.modal = function(modal) {
var overlay = document.createElement('div');
var closer = document.createElement('div');
var trigger = document.querySelector(modal.dataset.trigger);
var origParent = modal.parentNode;
function open(event) {
if (event) {
event.preventDefault();
}
modal.classList.add('modal--open');
overlay.classList.add('modal__overlay');
document.body.insertBefore(modal, null);
document.body.insertBefore(overlay, modal);
overlay.classList.add('modal__overlay--open');
toggleScroll(false);
if (modal.dataset.closeable === 'true') {
closer.classList.add('modal__closer');
modal.insertBefore(closer, null);
closer.addEventListener('click', close, false);
overlay.addEventListener('click', close, false);
}
}
// open this modal with an event:
// document.dispatchEvent(new CustomEvent('modal-open', { dateils: {for: 'modal-13'}}))
function openOnEvent(event) {
if (event.detail.for === modal.getAttribute('id')) {
open();
}
}
function close(event) {
if (typeof event === 'undefined' || event.target === closer || event.target === overlay) {
overlay.remove();
origParent.insertBefore(modal, null);
modal.classList.remove('modal--open');
toggleScroll(true);
closer.removeEventListener('click', close, false);
}
};
function setup() {
// every modal can be openend via document-wide event, see openOnEvent
document.addEventListener('modal-open', openOnEvent, false);
// if modal has trigger assigned to it open modal on click
if (trigger) {
trigger.classList.add('modal__trigger');
trigger.addEventListener('click', open, false);
}
// if there is no content specified for the modal we assume that
// the content is supposed to be the page the trigger links to.
// so we check if the trigger has a href-attribute, fetch that page
// and replace the modal content with the response
var replaceMe = modal.querySelector('.replace-me');
var replaceWith = trigger ? trigger.getAttribute('href') : '';
if (replaceMe) {
replaceMe.classList.remove('replace-me');
replaceMe.innerText = '...loading';
if (replaceWith.length > 0) {
fetch(replaceWith).then(function(response) {
return response.text();
}).then(function(body) {
var modalContent = document.createElement('div');
modalContent.innerHTML = body;
var main = modalContent.querySelector('.main__content');
if (main) {
replaceMe.innerText = '';
replaceMe.insertBefore(main, null);
} else {
replaceMe.innerHTML = body;
}
});
}
}
// tell further modals, that this one already got initialized
modal.classList.add('js-modal-initialized');
}
setup();
};
// make sure document doesn't scroll when modal is active
function toggleScroll(scrollable) {
document.body.classList.toggle('no-scroll', !scrollable);
}
})();
document.addEventListener('DOMContentLoaded', function() {
Array.from(document.querySelectorAll('.js-modal:not(.js-modal-initialized)')).map(function(modal) {
new utils.modal(modal);
});
}, false);

View File

@ -0,0 +1,87 @@
.modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0.8, 0.8);
display: block;
background-color: rgba(255, 255, 255, 0.9);
min-width: 60vw;
min-height: 100px;
max-height: calc(100vh - 30px);
border-radius: 7px;
z-index: -1;
color: var(--fontbase);
padding: 20px;
overflow: auto;
opacity: 0;
transition: all .15s ease;
&.modal--open {
opacity: 1;
z-index: 200;
transform: translate(-50%, -50%) scale(1, 1);
}
}
@media (max-width: 999px) {
.modal {
min-width: 80vw;
}
}
@media (max-width: 666px) {
.modal {
min-width: 90vw;
}
}
@media (max-width: 444px) {
.modal {
min-width: calc(100vw - 20px);
}
}
.modal__overlay {
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
background-color: transparent;
z-index: -1;
opacity: 0;
transition: all .2s ease;
&.modal__overlay--open {
z-index: 199;
opacity: 1;
background-color: rgba(0, 0, 0, 0.4);
}
}
.modal__trigger {
cursor: pointer;
}
.modal__closer {
position: absolute;
top: 20px;
right: 20px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
background-color: var(--darkbase);
border-radius: 2px;
cursor: pointer;
z-index: 20;
&::before {
content: '\e014';
font-family: 'Glyphicons Halflings';
color: white;
}
}
.no-scroll {
overflow: hidden;
}

View File

@ -1,88 +1,107 @@
/**
* delcare a table as sortable by adding class 'js-sortable'
*/
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.sortable = function(table) {
var ASC = 1;
var DESC = -1;
var trs, ths, sortBy, sortDir, trContents;
function setup() {
trs = table.querySelectorAll('tr');
ths = table.querySelectorAll('th');
sortBy = 0;
sortDir = ASC;
trContents = [];
Array.from(trs).forEach(function(tr, rowIndex) {
if (rowIndex === 0) {
// register table headers as sort-listener
Array.from(tr.querySelectorAll('th')).forEach(function(th, thIndex) {
th.addEventListener('click', function(el) {
sortTableBy(thIndex);
});
});
} else {
// register table rows
trContents.push(Array.from(tr.querySelectorAll('td')).map(function(td) {
return td.innerHTML;
}));
}
});
}
setup();
function updateThs(thIndex, sortOrder) {
Array.from(ths).forEach(function (th) {
th.classList.remove('sorted-asc', 'sorted-desc');
});
var suffix = sortOrder > 0 ? 'asc' : 'desc';
ths[thIndex].classList.add('sorted-' + suffix);
}
function sortTableBy(thIndex) {
var sortKey = thIndex;
var sortOrder = ASC;
if (sortBy === sortKey) {
sortOrder = sortDir === ASC ? DESC : ASC;
}
trContents.sort(dynamicSortByType(sortKey, sortOrder));
trContents.sort(dynamicSortByKey(sortKey, sortOrder));
sortBy = thIndex;
sortDir = sortOrder;
updateThs(thIndex, sortOrder);
Array.from(trs).forEach(function(tr, trIndex) {
if (trIndex > 0) {
Array.from(tr.querySelectorAll('td')).forEach(function (td, tdIndex) {
td.innerHTML = trContents[trIndex - 1][tdIndex];
});
}
});
}
function dynamicSortByKey(key, order) {
return function (a,b) {
var aVal = parseInt(a[key]);
var bVal = parseInt(b[key]);
if ((isNaN(aVal) && !isNaN(bVal)) || (!isNaN(aVal) && isNaN(bVal))) {
return 1;
}
aVal = isNaN(aVal) ? a[key] : aVal;
bVal = isNaN(bVal) ? b[key] : bVal;
var result = (aVal < bVal) ? -1 : (aVal > bVal) ? 1 : 0;
return result * order;
}
}
function dynamicSortByType(key, order) {
return function (a,b) {
var aVal = parseInt(a[key]);
var bVal = parseInt(b[key]);
aVal = isNaN(aVal) ? a[key] : aVal;
bVal = isNaN(bVal) ? b[key] : bVal;
var res = (aVal < bVal ? -1 : aVal > bVal ? 1 : 0);
if (isNaN(aVal) && !isNaN(bVal)) {
res = -1;
}
if (!isNaN(aVal) && isNaN(bVal)) {
res = 1;
}
return res * order;
}
}
};
})();
document.addEventListener('DOMContentLoaded', function() {
var tables = [];
var ASC = 1;
var DESC = -1;
function initTable(table, tableIndex) {
var trs = table.querySelectorAll('tr');
var ths = table.querySelectorAll('th');
var trContents = [];
Array.from(trs).forEach(function(tr, rowIndex) {
if (rowIndex === 0) {
// register table headers as sort-listener
Array.from(tr.querySelectorAll('th')).forEach(function(th, thIndex) {
th.addEventListener('click', function(el) {
sortTableBy(tableIndex, thIndex);
});
});
} else {
// register table rows
trContents.push(Array.from(tr.querySelectorAll('td')).map(function(td) {
return td.innerHTML;
}));
}
});
tables.push({
el: table,
ths: ths,
sortBy: 0,
sortDir: ASC,
trContents,
});
}
function updateThs(tableIndex, thIndex, sortOrder) {
Array.from(tables[tableIndex].ths).forEach(function (th) {
th.classList.remove('sorted-asc', 'sorted-desc');
});
var suffix = sortOrder > 0 ? 'asc' : 'desc';
tables[tableIndex].ths[thIndex].classList.add('sorted-' + suffix);
}
function sortTableBy(tableIndex, thIndex) {
var table = tables[tableIndex];
var sortKey = thIndex;
var sortOrder = ASC;
if (table.sortBy === sortKey) {
sortOrder = table.sortDir === ASC ? DESC : ASC;
}
table.trContents.sort(dynamicSort(sortKey, sortOrder));
tables[tableIndex].sortBy = thIndex;
tables[tableIndex].sortDir = sortOrder;
updateThs(tableIndex, thIndex, sortOrder);
Array.from(table.el.querySelectorAll('tr')).forEach(function(tr, trIndex) {
if (trIndex > 0) {
Array.from(tr.querySelectorAll('td')).forEach(function (td, tdIndex) {
td.innerHTML = table.trContents[trIndex - 1][tdIndex];
});
}
});
}
function dynamicSort(key, order) {
return function (a,b) {
var aVal = parseInt(a[key]);
var bVal = parseInt(b[key]);
if ((isNaN(aVal) && !isNaN(bVal)) || (!isNaN(aVal) && isNaN(bVal))) {
console.error('trying to sort table by row with mixed content: "%s", "%s"', a[key], b[key]);
}
aVal = isNaN(aVal) ? a[key] : aVal;
bVal = isNaN(bVal) ? b[key] : bVal;
var result = (aVal < bVal) ? -1 : (aVal > bVal) ? 1 : 0;
return result * order;
}
}
var rawTables = document.querySelectorAll('.js-sortable');
Array.from(rawTables).forEach(function(table, i) {
initTable(table, i);
Array.from(document.querySelectorAll('.js-sortable')).forEach(function(table) {
utils.sortable(table);
});
});

View File

@ -1,3 +1,4 @@
$newline never
<aside .main__aside>
<div .asidenav>
<div .asidenav__box--dont-hide>
@ -20,13 +21,28 @@
<a .asidenav__link-wrapper href="/course/S2018/ixd/show">
<div .asidenav__link-triple>IXD
<div .asidenav__link-label>Interaction Design
<ul .asidenav__nested-list>
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/ex">Übungsblätter
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/show">Klausuren
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ixd/show">Übungsgruppen
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ffp/show">
<div .asidenav__link-triple>FFP
<div .asidenav__link-label>Fortgeschrittene Funktionale Programmierung
<ul .asidenav__nested-list>
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ffp/ex">Abgaben
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ffp/show">Klausuren
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/dbs/show">
<div .asidenav__link-triple>DBS
<div .asidenav__link-label>Datenbanksysteme
<ul .asidenav__nested-list>
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/dbs/ex">Übungsgruppen
<div .asidenav__toggler>

View File

@ -3,9 +3,18 @@
window.utils = window.utils || {};
window.utils.aside = function(asideEl, topNav) {
// Defines a function to turn an element into an interactive aside-navigation.
// If the small is smaller than 999px the navigation is automatically
// collapsed - even when dynamically resized (e.g. switching from portatit
// to landscape).
// The can user may also manually collapse and expand the navigation by
// using the little arrow at the bottom.
window.utils.aside = function(asideEl) {
var collapsed = false;
var collClass = 'main__aside--collapsed';
// animClass used to enable transitions only when needed so that
// (potentially happening) initial collapse of the asidenav
// goes unnoticed by the user.
var animClass = 'main__aside--transitioning';
var aboveCollapsedNav = false;
@ -15,24 +24,14 @@
if (document.body.getBoundingClientRect().width < 999 || collLS) {
asideEl.classList.add(collClass);
collapsed = true;
if (topNav) {
topNav.style.paddingLeft = '90px';
window.setTimeout(function() {
topNav.classList.add('navbar--animated');
}, 200);
}
} else if (topNav) {
topNav.classList.add('navbar--animated');
}
addListener();
}
function check() {
if (collapsed && !hasClass() || !collapsed && hasClass()) {
asideEl.classList.add(animClass);
asideEl.classList.toggle(collClass, collapsed);
if (topNav) {
topNav.style.paddingLeft = collapsed ? '90px' : '';
}
window.localStorage.setItem('asidenavCollapsed', collapsed);
}
}
@ -41,41 +40,46 @@
return asideEl.classList.contains(collClass);
}
asideEl.querySelector('.asidenav__toggler').addEventListener('click', function(event) {
collapsed = !collapsed;
check();
}, false);
asideEl.addEventListener('transitionend', function(event) {
if (event.propertyName === 'opacity') {
asideEl.classList.remove(animClass);
}
}, false);
window.addEventListener('resize', function() {
collapsed = document.body.getBoundingClientRect().width < 999;
check();
}, false);
function addListener() {
asideEl.addEventListener('mouseover', function(event) {
if (!collapsed) {
return false;
}
aboveCollapsedNav = true;
console.log(event);
window.setTimeout(function() {
if (aboveCollapsedNav && !document.body.classList.contains('touch-supported')) {
asideEl.classList.add('pseudo-hover');
asideEl.querySelector('.asidenav__toggler').addEventListener('click', function(event) {
collapsed = !collapsed;
check();
}, false);
asideEl.addEventListener('transitionend', function(event) {
if (event.propertyName === 'opacity') {
asideEl.classList.remove(animClass);
}
}, 430);
}, false);
asideEl.addEventListener('mouseleave', function(event) {
aboveCollapsedNav = false;
asideEl.classList.remove('pseudo-hover');
}, false);
}, false);
window.addEventListener('resize', function() {
collapsed = document.body.getBoundingClientRect().width < 999;
check();
}, false);
asideEl.addEventListener('mouseover', function(event) {
if (!collapsed) {
return false;
}
aboveCollapsedNav = true;
window.setTimeout(function() {
if (aboveCollapsedNav && !document.body.classList.contains('touch-supported')) {
asideEl.classList.add('pseudo-hover');
}
}, 800);
}, false);
asideEl.addEventListener('mouseleave', function(event) {
aboveCollapsedNav = false;
asideEl.classList.remove('pseudo-hover');
}, false);
}
};
})();
document.addEventListener('DOMContentLoaded', function() {
utils.aside(document.querySelector('.main__aside'), document.querySelector('.navbar'));
utils.aside(document.querySelector('.main__aside'));
});

View File

@ -4,7 +4,6 @@
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1;
flex: 0 0 300px;
overflow: hidden;
}
.main__aside--transitioning {
transition: flex-basis .2s ease;
@ -19,6 +18,7 @@
.main__aside--collapsed {
width: 50px;
flex-basis: 50px;
overflow: hidden;
.asidenav__box-title {
width: 50px;
@ -44,6 +44,9 @@
margin-top: 20px;
color: white;
.js-show-hide__target {
overflow: visible;
}
.js-show-hide__toggle::before {
top: 14px;
right: 12px;
@ -76,6 +79,39 @@
}
}
.asidenav__nested-list {
position: absolute;
top: 0;
right: 0;
color: var(--fontbase);
transform: translateX(0);
opacity: 0;
transition: all .2s ease;
z-index: -1;
.asidenav__list-item {
background-color: var(--darkbase);
color: white;
&:first-child {
margin-top: 0;
}
}
.asidenav__link-wrapper {
padding-left: 13px;
padding-right: 13px;
border-left: 20px solid white;
transition: all .2s ease;
&:hover {
background-color: white;
color: var(--darkbase) !important;
border-left: 20px solid var(--darkbase);
}
}
}
.asidenav__list-item {
position: relative;
background-color: white;
@ -86,6 +122,11 @@
color: white;
background-color: var(--darkbase);
.asidenav__nested-list {
transform: translateX(100%);
opacity: 1;
}
.asidenav__link-wrapper,
.asidenav__link-label {
color: white;

View File

@ -0,0 +1,18 @@
<div .modal.js-modal #modal-#{modalId} data-trigger=#{modalTrigger} data-closeable=true>
$if 11 == length modalContent
<div .replace-me>
$else
<h2>Neue Veranstaltung
#{modalContent}
<form>
<div .form-group>
<label .reactive-label for="inp1">Name
<input type="text" id="inp1">
<div .form-group>
<label .reactive-label for="inp2">Kürzel
<input type="text" id="inp2">
<div .form-group>
<label .reactive-label for="inp3">Semester
<input type="text" id="inp3">
<div .form-group>
<input type="submit" value="Submit">

View File

@ -1,3 +1,4 @@
$newline never
<div .navbar-container>
<nav .navbar.js-sticky-navbar>

View File

@ -1,28 +1,48 @@
/**
* .js-sticky-navbar
* ul
* li Item 1
* li Item 2
*/
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.stickynav = function(nav) {
var ticking = false;
init();
function init() {
nav.style.paddingLeft = document.body.getBoundingClientRect().width < 999 ? '90px' : '';
window.setTimeout(function() {
nav.classList.add('navbar--animated');
}, 200);
checkScroll();
addListener();
}
// checks scroll direction and shows/hides navbar accordingly
function checkScroll() {
var sticky = window.scrollY > 0;
nav.classList.toggle('navbar--sticky', sticky);
ticking = false;
}
function addListener() {
window.addEventListener('scroll', function(e) {
if (!ticking) {
window.requestAnimationFrame(checkScroll);
ticking = true;
}
}, false);
}
window.addEventListener('resize', function() {
nav.style.paddingLeft = document.body.getBoundingClientRect().width < 999 ? '90px' : '';
}, false);
}
})();
document.addEventListener('DOMContentLoaded', function() {
var ticking = false;
var nav = document.querySelector('.js-sticky-navbar');
window.addEventListener('scroll', function(e) {
if (!ticking) {
window.requestAnimationFrame(checkScroll);
ticking = true;
}
}, false);
// checks scroll direction and shows/hides navbar accordingly
function checkScroll() {
var sticky = window.scrollY > 0;
nav.classList.toggle('navbar--sticky', sticky);
ticking = false;
}
checkScroll();
utils.stickynav(document.querySelector('.js-sticky-navbar'));
});

View File

@ -0,0 +1,11 @@
$newline never
$if hasPageActions
<div .page-nav-prime>
<h3>Aktionen:
<ul .pagenav__list>
$forall menuType <- menuTypes
$case menuType
$of PageActionPrime (MenuItem label mIcon route _)
<li .pagenav__list-item>
<a .pagenav__link-wrapper href=@{route}>#{label}
$of _

View File

@ -0,0 +1,21 @@
.page-nav-prime {
background-color: var(--lightgreybase);
box-shadow: -20px -20px 0 20px var(--lightgreybase),
20px -20px 0 20px var(--lightgreybase);
padding: 13px 0;
}
.page-nav-prime .pagenav__list {
margin: 7px 0 0;
display: block;
}
.page-nav-prime .pagenav__list-item {
display: inline-block;
border-bottom: 2px solid var(--lightbase);
margin-right: 7px;
transition: border-bottom-color .2s ease;
&:hover {
border-bottom-color: var(--lighterbase);
}
}