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 - i18n der
Links -> MenuItems verwenden wie bisher Links -> MenuItems verwenden wie bisher
Page Titles -> setTitleI 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? 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 ** Page pageActions - Berechtigungen prüfen?
- Berechtigungen prüfen?
=> Eigener Constructor statt NavbarLeft/Right?! => Eigener Constructor statt NavbarLeft/Right?!

View File

@ -87,19 +87,17 @@ main = db $ do
, courseTermId = TermKey summer2018 , courseTermId = TermKey summer2018
, courseSchoolId = ifi , courseSchoolId = ifi
, courseCapacity = Just 20 , courseCapacity = Just 20
, courseCreated = now
, courseChanged = now
, courseCreatedBy = gkleen
, courseChangedBy = gkleen
, courseHasRegistration = True , courseHasRegistration = True
, courseRegisterFrom = Just now , courseRegisterFrom = Just now
, courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now ) , courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now )
} }
insert_ $ CourseEdit jost now ffp
void . insert $ DegreeCourse ifiBsc ffp void . insert $ DegreeCourse ifiBsc ffp
void . insert $ DegreeCourse ifiMsc ffp void . insert $ DegreeCourse ifiMsc ffp
void . insert $ Lecturer gkleen ffp void . insert $ Lecturer gkleen ffp
void . insert $ Corrector gkleen ffp (ByProportion 1) insert_ $ Corrector gkleen ffp (ByProportion 1)
void . insert $ Sheet ffp "Blatt 1" Nothing NotGraded Nothing now now Nothing Nothing now now gkleen gkleen sheetkey <- insert $ Sheet ffp "Blatt 1" Nothing NotGraded NoGroups Nothing Nothing now now Nothing Nothing
insert_ $ SheetEdit gkleen now sheetkey
-- EIP -- EIP
eip <- insert Course eip <- insert Course
{ courseName = "Einführung in die Programmierung" { courseName = "Einführung in die Programmierung"
@ -109,14 +107,11 @@ main = db $ do
, courseTermId = TermKey summer2017 , courseTermId = TermKey summer2017
, courseSchoolId = ifi , courseSchoolId = ifi
, courseCapacity = Just 20 , courseCapacity = Just 20
, courseCreated = now
, courseChanged = now
, courseCreatedBy = fhamann
, courseChangedBy = fhamann
, courseHasRegistration = False , courseHasRegistration = False
, courseRegisterFrom = Nothing , courseRegisterFrom = Nothing
, courseRegisterTo = Nothing , courseRegisterTo = Nothing
} }
insert_ $ CourseEdit fhamann now eip
void . insert $ DegreeCourse ifiBsc eip void . insert $ DegreeCourse ifiBsc eip
void . insert $ DegreeCourse ifiMsc eip void . insert $ DegreeCourse ifiMsc eip
void . insert $ Lecturer fhamann eip void . insert $ Lecturer fhamann eip
@ -129,14 +124,11 @@ main = db $ do
, courseTermId = TermKey summer2018 , courseTermId = TermKey summer2018
, courseSchoolId = ifi , courseSchoolId = ifi
, courseCapacity = Just 20 , courseCapacity = Just 20
, courseCreated = now
, courseChanged = now
, courseCreatedBy = fhamann
, courseChangedBy = fhamann
, courseHasRegistration = True , courseHasRegistration = True
, courseRegisterFrom = Just now , courseRegisterFrom = Just now
, courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now ) , courseRegisterTo = Just ((3600 * 24 * 60) `addUTCTime` now )
} }
insert_ $ CourseEdit fhamann now ixd
void . insert $ DegreeCourse ifiBsc ixd void . insert $ DegreeCourse ifiBsc ixd
void . insert $ Lecturer fhamann ixd void . insert $ Lecturer fhamann ixd
-- concept development -- concept development
@ -148,14 +140,11 @@ main = db $ do
, courseTermId = TermKey winter2017 , courseTermId = TermKey winter2017
, courseSchoolId = ifi , courseSchoolId = ifi
, courseCapacity = Just 30 , courseCapacity = Just 30
, courseCreated = now
, courseChanged = now
, courseCreatedBy = fhamann
, courseChangedBy = fhamann
, courseHasRegistration = False , courseHasRegistration = False
, courseRegisterFrom = Nothing , courseRegisterFrom = Nothing
, courseRegisterTo = Nothing , courseRegisterTo = Nothing
} }
insert_ $ CourseEdit fhamann now ux3
void . insert $ DegreeCourse ifiBsc ux3 void . insert $ DegreeCourse ifiBsc ux3
void . insert $ Lecturer fhamann ux3 void . insert $ Lecturer fhamann ux3
-- promo -- promo
@ -167,14 +156,11 @@ main = db $ do
, courseTermId = TermKey summer2017 , courseTermId = TermKey summer2017
, courseSchoolId = ifi , courseSchoolId = ifi
, courseCapacity = Just 50 , courseCapacity = Just 50
, courseCreated = now
, courseChanged = now
, courseCreatedBy = jost
, courseChangedBy = jost
, courseHasRegistration = False , courseHasRegistration = False
, courseRegisterFrom = Nothing , courseRegisterFrom = Nothing
, courseRegisterTo = Nothing , courseRegisterTo = Nothing
} }
insert_ $ CourseEdit jost now pmo
void . insert $ DegreeCourse ifiBsc pmo void . insert $ DegreeCourse ifiBsc pmo
void . insert $ Lecturer jost pmo void . insert $ Lecturer jost pmo
-- datenbanksysteme -- datenbanksysteme
@ -186,13 +172,11 @@ main = db $ do
, courseTermId = TermKey summer2018 , courseTermId = TermKey summer2018
, courseSchoolId = ifi , courseSchoolId = ifi
, courseCapacity = Just 50 , courseCapacity = Just 50
, courseCreated = now
, courseChanged = now
, courseCreatedBy = jost
, courseChangedBy = jost
, courseHasRegistration = False , courseHasRegistration = False
, courseRegisterFrom = Nothing , courseRegisterFrom = Nothing
, courseRegisterTo = Nothing , courseRegisterTo = Nothing
} }
insert_ $ CourseEdit gkleen now dbs
void . insert $ DegreeCourse ifiBsc 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 termId TermId
schoolId SchoolId schoolId SchoolId
capacity Int Maybe capacity Int Maybe
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo hasRegistration Bool -- canRegisterNow = hasRegistration && maybe False (<= currentTime) registerFrom && maybe True (>= currentTime) registerTo
registerFrom UTCTime Maybe registerFrom UTCTime Maybe
registerTo UTCTime Maybe registerTo UTCTime Maybe
CourseTermShort termId shorthand CourseTermShort termId shorthand
CourseEdit
user UserId
time UTCTime
course CourseId
Lecturer Lecturer
userId UserId userId UserId
courseId CourseId courseId CourseId
@ -103,15 +103,11 @@ Sheet
activeTo UTCTime activeTo UTCTime
hintFrom UTCTime Maybe hintFrom UTCTime Maybe
solutionFrom UTCTime Maybe solutionFrom UTCTime Maybe
created UTCTime -- delete
changed UTCTime -- delete
createdBy UserId -- delete
changedBy UserId -- delete
CourseSheet courseId name CourseSheet courseId name
SheetEdit SheetEdit
sheet SheetId
user UserId user UserId
time UTCTime time UTCTime
sheet SheetId
SheetFile SheetFile
sheetId SheetId sheetId SheetId
fileId FileId fileId FileId
@ -128,11 +124,11 @@ Submission
ratingComment Text Maybe ratingComment Text Maybe
ratingBy UserId Maybe ratingBy UserId Maybe
ratingTime UTCTime Maybe ratingTime UTCTime Maybe
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
deriving Show deriving Show
SubmissionEdit
user UserId
time UTCTime
submission SubmissionId
SubmissionFile SubmissionFile
submissionId SubmissionId submissionId SubmissionId
fileId FileId fileId FileId
@ -147,10 +143,10 @@ SubmissionUser
SubmissionGroup SubmissionGroup
courseId CourseId courseId CourseId
name Text name Text
created UTCTime SubmissionGroupEdit
changed UTCTime user UserId
createdBy UserId time UTCTime
changedBy UserId submissionGroup SubmissionGroupId
SubmissionGroupUser SubmissionGroupUser
submissionGroupId SubmissionGroupId submissionGroupId SubmissionGroupId
userId UserId userId UserId
@ -169,13 +165,12 @@ Booking
end UTCTime end UTCTime
weekly Bool weekly Bool
exceptions [Day] -- only if weekly, begin in exception exceptions [Day] -- only if weekly, begin in exception
created UTCTime
changed UTCTime
createdBy UserId
changedBy UserId
bookedFor RoomForId bookedFor RoomForId
room RoomId room RoomId
BookingEdit
user UserId
time UTCTime
boooking BookingId
Room Room
name Text name Text
capacity Int Maybe capacity Int Maybe
@ -201,10 +196,10 @@ Exam
deregistrationEnd UTCTime deregistrationEnd UTCTime
ratingVisible Bool ratingVisible Bool
statisticsVisible Bool statisticsVisible Bool
created UTCTime ExamEdit
changed UTCTime user UserId
createdBy UserId time UTCTime
changedBy UserId exam ExamId
ExamUser ExamUser
userId UserId userId UserId
examId ExamId examId ExamId

39
routes
View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ViewPatterns #-}
module Handler.Submission where module Handler.Submission where
@ -53,7 +54,7 @@ submissionTable = do
(,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s (,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s
let let
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseShowR courseTermId courseShorthand anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseR courseTermId courseShorthand CourseShowR
courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName
anchorSubmission (_, cUUID, _) = SubmissionR cUUID anchorSubmission (_, cUUID, _) = SubmissionR cUUID
submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID
@ -177,8 +178,12 @@ postSubmissionDownloadMultiArchiveR = do
withinDirectory f@File{..} = f { fileTitle = directoryName </> fileTitle } 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 yield $ File
{ fileModified = submissionChanged { fileModified = lastEditTime
, fileTitle = directoryName , fileTitle = directoryName
, fileContent = Nothing , fileContent = Nothing
} }

View File

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

View File

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

View File

@ -6,3 +6,17 @@ import Import.NoFoundation
lipsum :: WidgetT site IO () lipsum :: WidgetT site IO ()
lipsum = $(widgetFile "widgets/lipsum") 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" derivePersistFieldJSON "SheetType"
data SheetGroup data SheetGroup
= Arbitrary { maxParticipants :: Int } = Arbitrary { maxParticipants :: Int } -- Distinguish Limited/Arbitrary
| RegisteredGroups | RegisteredGroups
| NoGroups | NoGroups
deriving (Show, Read, Eq) deriving (Show, Read, Eq)

View File

@ -16,7 +16,7 @@
\ bis #{formatTimeGerWD regTo} \ bis #{formatTimeGerWD regTo}
<div> <div>
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}> <form method=post action=@{CourseR tid csh CourseShowR} enctype=#{regEnctype}>
^{regWidget} ^{regWidget}
<div .course-header__title> <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} ^{pageBody pc}
$maybe analytics <- appAnalytics $ appSettings master $maybe analytics <- appAnalytics $ appSettings master

View File

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

View File

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

View File

@ -39,7 +39,7 @@
<tbody> <tbody>
<tr> <tr>
<td>0 <td>0
<td>14 <td>NT2
<td>CON2 <td>CON2
<td>3 <td>3
<tr> <tr>
@ -57,6 +57,11 @@
<td>43 <td>43
<td>T2C2 <td>T2C2
<td>35 <td>35
<tr>
<td>4
<td>73
<td>CA62
<td>7
<hr> <hr>
<div .container> <div .container>
@ -67,3 +72,29 @@
Knopf-Test: Knopf-Test:
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}> <form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
^{btnWdgt} ^{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 $forall fileLink <- fileLinks
<li> <li>
$case fileLink $case fileLink
$of SheetFileR _ _ _ typ name $of CourseR _ _ (SheetR (SheetFileR _ typ name))
#{toPathPiece typ} #{toPathPiece typ}
<a href=@{fileLink}>#{name} <a href=@{fileLink}>#{name}
$of other $of other

View File

@ -24,10 +24,12 @@
}); });
}; };
// allows for multiple file uploads with separate inputs
window.utils.reactiveFileUpload = function(input, parent) { window.utils.reactiveFileUpload = function(input, parent) {
var currValidInputCount = 0; var currValidInputCount = 0;
var addMore = false; var addMore = false;
var inputName = input.getAttribute('name'); var inputName = input.getAttribute('name');
var isMulti = input.getAttribute('multiple') ? true : false;
// FileInput PseudoClass // FileInput PseudoClass
function FileInput(container, input, label, remover) { function FileInput(container, input, label, remover) {
this.container = container; this.container = container;
@ -42,7 +44,7 @@
this.remove = function() { this.remove = function() {
this.container.remove(); this.container.remove();
} }
this.isValid = function() { this.wasValid = function() {
return this.container.classList.contains('file-input__container--valid'); return this.container.classList.contains('file-input__container--valid');
} }
} }
@ -61,7 +63,9 @@
parent.classList.add('form-group--valid') parent.classList.add('form-group--valid')
} }
submitBtn.removeAttribute('disabled'); submitBtn.removeAttribute('disabled');
addNextInput(); if (isMulti) {
addNextInput();
}
} else { } else {
if (parent.classList.contains('form-group')) { if (parent.classList.contains('form-group')) {
parent.classList.remove('form-group--valid') parent.classList.remove('form-group--valid')
@ -78,14 +82,16 @@
var fileName = filePath[filePath.length - 1]; var fileName = filePath[filePath.length - 1];
fileInput.label.innerHTML = fileName; fileInput.label.innerHTML = fileName;
// increase count if this field was empty previously // increase count if this field was empty previously
if (!fileInput.isValid()) { if (!fileInput.wasValid()) {
currValidInputCount++; currValidInputCount++;
} }
fileInput.container.classList.add('file-input__container--valid') fileInput.container.classList.add('file-input__container--valid')
// show next input // show next input
} else { } else {
currValidInputCount--; if (isMulti) {
fileInput.remove(); currValidInputCount--;
}
clearInput(fileInput);
} }
updateForm(); updateForm();
}); });
@ -95,26 +101,36 @@
fileInput.input.addEventListener('blur', function() { fileInput.input.addEventListener('blur', function() {
fileInput.container.classList.remove('pseudo-focus'); fileInput.container.classList.remove('pseudo-focus');
}); });
fileInput.label.addEventListener('click', function() {
fileInput.input.click();
});
fileInput.remover.addEventListener('click', function() { fileInput.remover.addEventListener('click', function() {
if (fileInput.isValid()) { if (fileInput.wasValid()) {
currValidInputCount--; currValidInputCount--;
} }
fileInput.remove(); clearInput(fileInput);
updateForm();
}); });
} }
// 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 // create new wrapped input element with name name
function makeInput(name) { function makeInput(name) {
var cont = document.createElement('div'); var cont = document.createElement('div');
var desc = document.createElement('span'); var desc = document.createElement('label');
var nextInput = document.createElement('input'); var nextInput = document.createElement('input');
var remover = document.createElement('div'); var remover = document.createElement('div');
cont.classList.add('file-input__container'); cont.classList.add('file-input__container');
desc.classList.add('file-input__label', 'btn'); desc.classList.add('file-input__label', 'btn');
nextInput.classList.add('js-file-input');
desc.setAttribute('for', name + '-' + currValidInputCount);
remover.classList.add('file-input__remover'); remover.classList.add('file-input__remover');
nextInput.setAttribute('id', name + '-' + currValidInputCount);
nextInput.setAttribute('name', name); nextInput.setAttribute('name', name);
nextInput.setAttribute('type', 'file'); nextInput.setAttribute('type', 'file');
cont.appendChild(nextInput); cont.appendChild(nextInput);
@ -132,6 +148,39 @@
setup(); 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) { window.utils.reactiveFormGroup = function(formGroup, input) {
// updates to dom // updates to dom
if (input.value.length > 0) { if (input.value.length > 0) {
@ -156,18 +205,28 @@ document.addEventListener('DOMContentLoaded', function() {
// setup reactive labels // setup reactive labels
Array.from(document.querySelectorAll('.reactive-label')).forEach(function(label) { Array.from(document.querySelectorAll('.reactive-label')).forEach(function(label) {
var input = document.querySelector('#' + label.getAttribute('for')); 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 parent = label.parentElement;
var type = input.getAttribute('type'); 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 isListening = !RegExp(['date', 'checkbox', 'radio', 'hidden', 'file'].join('|')).test(type);
var isInFormGroup = parent.classList.contains('form-group') && parent.classList.contains('form-group--required'); var isInFormGroup = parent.classList.contains('form-group') && parent.classList.contains('form-group--required');
if (isInFormGroup) { if (isInFormGroup) {
window.utils.reactiveFormGroup(parent, input); window.utils.reactiveFormGroup(parent, input);
} }
if (isFileInput) { if (isFileUpload) {
window.utils.reactiveFileUpload(input, parent); window.utils.reactiveFileUpload(input, parent);
} }
if (isFileCheckbox) {
window.utils.reactiveFileCheckbox(input, label, parent);
}
if (isListening) { if (isListening) {
window.utils.reactiveInputLabel(input, label); window.utils.reactiveInputLabel(input, label);
} else { } else {

View File

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

View File

@ -1,3 +1,4 @@
$newline never
<aside .main__aside> <aside .main__aside>
<div .asidenav> <div .asidenav>
<div .asidenav__box--dont-hide> <div .asidenav__box--dont-hide>
@ -20,13 +21,28 @@
<a .asidenav__link-wrapper href="/course/S2018/ixd/show"> <a .asidenav__link-wrapper href="/course/S2018/ixd/show">
<div .asidenav__link-triple>IXD <div .asidenav__link-triple>IXD
<div .asidenav__link-label>Interaction Design <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> <li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/ffp/show"> <a .asidenav__link-wrapper href="/course/S2018/ffp/show">
<div .asidenav__link-triple>FFP <div .asidenav__link-triple>FFP
<div .asidenav__link-label>Fortgeschrittene Funktionale Programmierung <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> <li .asidenav__list-item>
<a .asidenav__link-wrapper href="/course/S2018/dbs/show"> <a .asidenav__link-wrapper href="/course/S2018/dbs/show">
<div .asidenav__link-triple>DBS <div .asidenav__link-triple>DBS
<div .asidenav__link-label>Datenbanksysteme <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> <div .asidenav__toggler>

View File

@ -3,9 +3,18 @@
window.utils = window.utils || {}; 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 collapsed = false;
var collClass = 'main__aside--collapsed'; 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 animClass = 'main__aside--transitioning';
var aboveCollapsedNav = false; var aboveCollapsedNav = false;
@ -15,24 +24,14 @@
if (document.body.getBoundingClientRect().width < 999 || collLS) { if (document.body.getBoundingClientRect().width < 999 || collLS) {
asideEl.classList.add(collClass); asideEl.classList.add(collClass);
collapsed = true; 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() { function check() {
if (collapsed && !hasClass() || !collapsed && hasClass()) { if (collapsed && !hasClass() || !collapsed && hasClass()) {
asideEl.classList.add(animClass); asideEl.classList.add(animClass);
asideEl.classList.toggle(collClass, collapsed); asideEl.classList.toggle(collClass, collapsed);
if (topNav) {
topNav.style.paddingLeft = collapsed ? '90px' : '';
}
window.localStorage.setItem('asidenavCollapsed', collapsed); window.localStorage.setItem('asidenavCollapsed', collapsed);
} }
} }
@ -41,41 +40,46 @@
return asideEl.classList.contains(collClass); return asideEl.classList.contains(collClass);
} }
asideEl.querySelector('.asidenav__toggler').addEventListener('click', function(event) { function addListener() {
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);
asideEl.addEventListener('mouseover', function(event) { asideEl.querySelector('.asidenav__toggler').addEventListener('click', function(event) {
if (!collapsed) { collapsed = !collapsed;
return false; check();
} }, false);
aboveCollapsedNav = true;
console.log(event); asideEl.addEventListener('transitionend', function(event) {
window.setTimeout(function() { if (event.propertyName === 'opacity') {
if (aboveCollapsedNav && !document.body.classList.contains('touch-supported')) { asideEl.classList.remove(animClass);
asideEl.classList.add('pseudo-hover');
} }
}, 430); }, false);
}, false);
asideEl.addEventListener('mouseleave', function(event) { window.addEventListener('resize', function() {
aboveCollapsedNav = false; collapsed = document.body.getBoundingClientRect().width < 999;
asideEl.classList.remove('pseudo-hover'); check();
}, false); }, 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() { 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); box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1; z-index: 1;
flex: 0 0 300px; flex: 0 0 300px;
overflow: hidden;
} }
.main__aside--transitioning { .main__aside--transitioning {
transition: flex-basis .2s ease; transition: flex-basis .2s ease;
@ -19,6 +18,7 @@
.main__aside--collapsed { .main__aside--collapsed {
width: 50px; width: 50px;
flex-basis: 50px; flex-basis: 50px;
overflow: hidden;
.asidenav__box-title { .asidenav__box-title {
width: 50px; width: 50px;
@ -44,6 +44,9 @@
margin-top: 20px; margin-top: 20px;
color: white; color: white;
.js-show-hide__target {
overflow: visible;
}
.js-show-hide__toggle::before { .js-show-hide__toggle::before {
top: 14px; top: 14px;
right: 12px; 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 { .asidenav__list-item {
position: relative; position: relative;
background-color: white; background-color: white;
@ -86,6 +122,11 @@
color: white; color: white;
background-color: var(--darkbase); background-color: var(--darkbase);
.asidenav__nested-list {
transform: translateX(100%);
opacity: 1;
}
.asidenav__link-wrapper, .asidenav__link-wrapper,
.asidenav__link-label { .asidenav__link-label {
color: white; 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> <div .navbar-container>
<nav .navbar.js-sticky-navbar> <nav .navbar.js-sticky-navbar>

View File

@ -1,28 +1,48 @@
/** (function() {
* .js-sticky-navbar 'use strict';
* ul
* li Item 1 window.utils = window.utils || {};
* li Item 2
*/ 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() { document.addEventListener('DOMContentLoaded', function() {
var ticking = false; utils.stickynav(document.querySelector('.js-sticky-navbar'));
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();
}); });

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);
}
}