Merge branch 'initial_thoughts_on_frontend' into 'master'

bootstrap-less frontend

Closes #29 and #18

See merge request !10
This commit is contained in:
Felix Hamann 2018-03-18 03:01:09 +01:00
commit 303f2163b0
45 changed files with 1736 additions and 7475 deletions

View File

@ -51,6 +51,8 @@ import Handler.Utils.StudyFeatures
import System.FilePath import System.FilePath
import Handler.Utils.Templates
-- | 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,6 +82,7 @@ mkYesodData "UniWorX" $(parseRoutesFile "routes")
data MenuItem = MenuItem data MenuItem = MenuItem
{ menuItemLabel :: Text { menuItemLabel :: Text
, menuItemIcon :: Maybe Text
, menuItemRoute :: Route UniWorX , menuItemRoute :: Route UniWorX
, menuItemAccessCallback :: Handler Bool , menuItemAccessCallback :: Handler Bool
} }
@ -88,6 +91,7 @@ data MenuTypes
= NavbarLeft { menuItem :: MenuItem } = NavbarLeft { menuItem :: MenuItem }
| NavbarRight { menuItem :: MenuItem } | NavbarRight { menuItem :: MenuItem }
| NavbarExtra { menuItem :: MenuItem } | NavbarExtra { menuItem :: MenuItem }
| NavbarSecondary { menuItem :: MenuItem }
-- | A convenient synonym for creating forms. -- | A convenient synonym for creating forms.
type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget) type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget)
@ -219,10 +223,10 @@ submissionAccess cID = do
adminAccess :: Maybe SchoolId -- ^ If @Just@, matched exactly against 'userAdminSchool' adminAccess :: Maybe SchoolId -- ^ If @Just@, matched exactly against 'userAdminSchool'
-> YesodDB UniWorX AuthResult -> YesodDB UniWorX AuthResult
adminAccess school = do adminAccess school = do
authId <- lift requireAuthId authId <- lift requireAuthId
adrights <- selectList ((UserAdminUser ==. authId) : maybe [] (\s -> [UserAdminSchool ==. s]) school) [] adrights <- selectList ((UserAdminUser ==. authId) : maybe [] (\s -> [UserAdminSchool ==. s]) school) []
return $ if (not $ null adrights) return $ if (not $ null adrights)
then Authorized then Authorized
else Unauthorized "No admin access" else Unauthorized "No admin access"
@ -230,7 +234,7 @@ lecturerAccess :: Maybe SchoolId
-> YesodDB UniWorX AuthResult -> YesodDB UniWorX AuthResult
lecturerAccess school = do lecturerAccess school = do
authId <- lift requireAuthId authId <- lift requireAuthId
lecrights <- selectList ((UserLecturerUser ==. authId) : maybe [] (\s -> [UserLecturerSchool ==. s]) school) [] lecrights <- selectList ((UserLecturerUser ==. authId) : maybe [] (\s -> [UserLecturerSchool ==. s]) school) []
return $ if (not $ null lecrights) return $ if (not $ null lecrights)
then Authorized then Authorized
else Unauthorized "No lecturer access" else Unauthorized "No lecturer access"
@ -250,15 +254,15 @@ isAuthorized' :: Route UniWorX -> Bool -> Handler Bool
isAuthorized' route isWrite = runDB $ isAuthorizedDB' route isWrite isAuthorized' route isWrite = runDB $ isAuthorizedDB' route isWrite
-- Define breadcrumbs. -- Define breadcrumbs.
instance YesodBreadcrumbs UniWorX where instance YesodBreadcrumbs UniWorX where
breadcrumb TermShowR = return ("Semester", Just HomeR) breadcrumb TermShowR = return ("Semester", Just HomeR)
breadcrumb TermEditR = return ("Neu", Just TermShowR) breadcrumb TermEditR = return ("Neu", Just TermShowR)
breadcrumb (TermEditExistR _) = return ("Editieren", Just TermShowR) breadcrumb (TermEditExistR _) = return ("Editieren", Just TermShowR)
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 (CourseShowR term course) = return (course, Just $ CourseListTermR term)
breadcrumb CourseEditR = return ("Neu", Just CourseListR) breadcrumb CourseEditR = return ("Neu", Just CourseListR)
breadcrumb (CourseEditExistR _ _) = return ("Editieren", Just CourseListR) breadcrumb (CourseEditExistR _ _) = return ("Editieren", Just CourseListR)
breadcrumb (SheetListR tid csh) = return ("Kurs", Just $ CourseShowR tid csh) breadcrumb (SheetListR tid csh) = return ("Kurs", Just $ CourseShowR tid csh)
@ -266,42 +270,48 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR) breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR) breadcrumb (SubmissionR _) = return ("Abgabe", Just SubmissionListR)
breadcrumb HomeR = return ("ReWorX", Nothing) breadcrumb HomeR = return ("ReWorX", Nothing)
breadcrumb (AuthR _) = return ("Login", Just HomeR) breadcrumb (AuthR _) = return ("Login", Just HomeR)
breadcrumb ProfileR = return ("Profile", Just HomeR) breadcrumb ProfileR = return ("Profile", Just HomeR)
breadcrumb _ = return ("home", Nothing) breadcrumb _ = return ("home", Nothing)
defaultLinks :: [MenuTypes] defaultLinks :: [MenuTypes]
defaultLinks = -- Define the menu items of the header. defaultLinks = -- Define the menu items of the header.
[ NavbarLeft $ MenuItem [ NavbarRight $ MenuItem
{ menuItemLabel = "Home" { menuItemLabel = "Home"
, menuItemIcon = Just "home"
, menuItemRoute = HomeR , menuItemRoute = HomeR
, menuItemAccessCallback = return True , menuItemAccessCallback = return True
} }
, NavbarLeft $ MenuItem , NavbarLeft $ MenuItem
{ menuItemLabel = "Kurse" { menuItemLabel = "Kurse"
, menuItemIcon = Just "book"
, menuItemRoute = CourseListR , menuItemRoute = CourseListR
, menuItemAccessCallback = return True , menuItemAccessCallback = return True
} }
, NavbarRight $ MenuItem , NavbarLeft $ MenuItem
{ menuItemLabel = "Users" { menuItemLabel = "Users"
, menuItemIcon = Just "user"
, menuItemRoute = UsersR , menuItemRoute = UsersR
, menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False , menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
} }
, NavbarRight $ MenuItem , NavbarRight $ MenuItem
{ menuItemLabel = "Profile" { menuItemLabel = "Profile"
, menuItemIcon = Just "user"
, menuItemRoute = ProfileR , menuItemRoute = ProfileR
, menuItemAccessCallback = isJust <$> maybeAuthPair , menuItemAccessCallback = isJust <$> maybeAuthPair
} }
, NavbarRight $ MenuItem , NavbarSecondary $ MenuItem
{ menuItemLabel = "Login" { menuItemLabel = "Login"
, menuItemIcon = Just "login"
, menuItemRoute = AuthR LoginR , menuItemRoute = AuthR LoginR
, menuItemAccessCallback = isNothing <$> maybeAuthPair , menuItemAccessCallback = isNothing <$> maybeAuthPair
} }
, NavbarRight $ MenuItem , NavbarSecondary $ MenuItem
{ menuItemLabel = "Logout" { menuItemLabel = "Logout"
, menuItemIcon = Just "logout"
, menuItemRoute = AuthR LogoutR , menuItemRoute = AuthR LogoutR
, menuItemAccessCallback = isJust <$> maybeAuthPair , menuItemAccessCallback = isJust <$> maybeAuthPair
} }
@ -328,12 +338,24 @@ defaultMenuLayout menu widget = do
-- value passed to hamletToRepHtml cannot be a widget, this allows -- value passed to hamletToRepHtml cannot be a widget, this allows
-- you to use normal widget features in default-layout. -- you to use normal widget features in default-layout.
let
navbar :: Widget
navbar = $(widgetFile "widgets/navbar")
asidenav :: Widget
asidenav = $(widgetFile "widgets/asidenav")
breadcrumbs :: Widget
breadcrumbs = $(widgetFile "widgets/breadcrumbs")
pc <- widgetToPageContent $ do pc <- widgetToPageContent $ do
addStylesheet $ StaticR css_bootstrap_css addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900"
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_icons_css
$(widgetFile "default-layout") $(widgetFile "default-layout")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/sortable")
$(widgetFile "standalone/inputs")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
-- How to run database actions. -- How to run database actions.
instance YesodPersist UniWorX where instance YesodPersist UniWorX where
type YesodPersistBackend UniWorX = SqlBackend type YesodPersistBackend UniWorX = SqlBackend
@ -363,7 +385,7 @@ instance YesodAuth UniWorX where
$logDebugS "auth" $ tshow ((userPlugin, userIdent), creds) $logDebugS "auth" $ tshow ((userPlugin, userIdent), creds)
when isDummy . (throwError =<<) . lift $ when isDummy . (throwError =<<) . lift $
maybe (UserError $ IdentifierNotFound credsIdent) (Authenticated . entityKey) <$> getBy uAuth maybe (UserError $ IdentifierNotFound credsIdent) (Authenticated . entityKey) <$> getBy uAuth
let let
@ -373,7 +395,7 @@ instance YesodAuth UniWorX where
userEmail <- maybe (throwError $ ServerError "Could not retrieve user email") return userEmail' userEmail <- maybe (throwError $ ServerError "Could not retrieve user email") return userEmail'
userDisplayName <- maybe (throwError $ ServerError "Could not retrieve user name") return userDisplayName' userDisplayName <- maybe (throwError $ ServerError "Could not retrieve user name") return userDisplayName'
let let
newUser = User{..} newUser = User{..}
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
@ -388,13 +410,13 @@ instance YesodAuth UniWorX where
userStudyFeatures' = [ v | (k, v) <- credsExtra, k == "dfnEduPersonFeaturesOfStudy" ] userStudyFeatures' = [ v | (k, v) <- credsExtra, k == "dfnEduPersonFeaturesOfStudy" ]
fs <- either (\err -> throwError . ServerError $ "Could not parse features of study: " <> err) return userStudyFeatures fs <- either (\err -> throwError . ServerError $ "Could not parse features of study: " <> err) return userStudyFeatures
lift $ deleteWhere [StudyFeaturesUser ==. userId] lift $ deleteWhere [StudyFeaturesUser ==. userId]
forM_ fs $ \StudyFeatures{..} -> do forM_ fs $ \StudyFeatures{..} -> do
lift . insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing lift . insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing
lift . insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing lift . insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing
lift $ insertMany_ fs lift $ insertMany_ fs
return $ Authenticated userId return $ Authenticated userId
where where
@ -419,7 +441,7 @@ ldapConfig _app@(appSettings -> settings) = LDAPConfig
} }
where where
principalName :: IsString a => a principalName :: IsString a => a
principalName = "userPrincipalName" principalName = "userPrincipalName"
identifierModifier _ entry = case lookup principalName $ leattrs entry of identifierModifier _ entry = case lookup principalName $ leattrs entry of
Just [n] -> Text.pack n Just [n] -> Text.pack n
_ -> error "Could not determine user principal name" _ -> error "Could not determine user principal name"

View File

@ -9,38 +9,38 @@
module Handler.Course where module Handler.Course where
import Import import Import
import Handler.Utils import Handler.Utils
-- import Data.Time -- import Data.Time
import qualified Data.Text as T import qualified Data.Text as T
import Data.Function ((&)) import Data.Function ((&))
import Yesod.Form.Bootstrap3 import Yesod.Form.Bootstrap3
import Colonnade hiding (fromMaybe) import Colonnade hiding (fromMaybe)
import Yesod.Colonnade import Yesod.Colonnade
import qualified Data.UUID.Cryptographic as UUID import qualified Data.UUID.Cryptographic as UUID
getCourseListR :: Handler TypedContent getCourseListR :: Handler TypedContent
getCourseListR = redirect TermShowR getCourseListR = redirect TermShowR
getCourseListTermR :: TermId -> Handler Html getCourseListTermR :: TermId -> Handler Html
getCourseListTermR tidini = do getCourseListTermR tidini = do
(term,courses) <- runDB $ (,) (term,courses) <- runDB $ (,)
<$> get tidini <$> get tidini
<*> selectList [CourseTermId ==. tidini] [Asc CourseShorthand] <*> selectList [CourseTermId ==. tidini] [Asc CourseShorthand]
when (isNothing term) $ do when (isNothing term) $ do
addMessage "warning" [shamlet| Semester #{toPathPiece tidini} nicht gefunden. |] addMessage "warning" [shamlet| Semester #{toPathPiece tidini} nicht gefunden. |]
redirect TermShowR redirect TermShowR
-- TODO: several runDBs per TableRow are probably too inefficient! -- TODO: several runDBs per TableRow are probably too inefficient!
let colonnadeTerms = mconcat let colonnadeTerms = mconcat
[ headed "Kürzel" $ (\ckv -> [ headed "Kürzel" $ (\ckv ->
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=@{CourseShowR tid shd}>#{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
@ -49,60 +49,61 @@ getCourseListTermR tidini = do
partiNum <- handlerToWidget $ runDB $ count [CourseParticipantCourseId ==. cid] partiNum <- handlerToWidget $ runDB $ count [CourseParticipantCourseId ==. cid]
[whamlet| #{show partiNum} |] [whamlet| #{show partiNum} |]
) )
, headed " " $ (\ckv -> , headed " " $ (\ckv ->
let c = entityVal ckv let c = entityVal ckv
shd = courseShorthand c shd = courseShorthand c
tid = courseTermId c tid = courseTermId c
in do in do
adminLink <- handlerToWidget $ isAuthorized (CourseEditExistR tid shd ) False adminLink <- handlerToWidget $ isAuthorized (CourseEditExistR tid shd ) False
-- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditExistR tid shd) else "" -- if (adminLink==Authorized) then linkButton "Ändern" BCWarning (CourseEditExistR tid shd) else ""
[whamlet| [whamlet|
$if adminLink == Authorized $if adminLink == Authorized
<a href=@{CourseEditExistR tid shd}> <a href=@{CourseEditExistR tid shd}>
editieren editieren
|] |]
) )
] ]
let pageLinks = let pageLinks =
[ NavbarLeft $ MenuItem [ NavbarLeft $ MenuItem
{ menuItemLabel = "Neuer Kurs" { menuItemLabel = "Neuer Kurs"
, menuItemIcon = Just "book"
, menuItemRoute = CourseEditR , menuItemRoute = CourseEditR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseEditR False , menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseEditR False
} }
] ]
let coursesTable = encodeWidgetTable tableSortable colonnadeTerms courses
defaultLinkLayout pageLinks $ do defaultLinkLayout pageLinks $ do
-- defaultLayout $ do -- defaultLayout $ do
setTitle "Semesterkurse" setTitle "Semesterkurse"
linkButton "Neuen Kurs anlegen" BCPrimary CourseEditR $(widgetFile "courses")
encodeWidgetTable tableDefault colonnadeTerms courses -- (map entityVal courses)
getCourseShowR :: TermId -> Text -> Handler Html getCourseShowR :: TermId -> Text -> Handler Html
getCourseShowR tid csh = do getCourseShowR tid csh = do
mbAid <- maybeAuthId mbAid <- maybeAuthId
(courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do (courseEnt,(schoolMB,participants,mbRegistered)) <- runDB $ do
courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort tid csh courseEnt@(Entity cid course) <- getBy404 $ CourseTermShort tid csh
dependent <- (,,) dependent <- (,,)
<$> get (courseSchoolId course) -- join <$> get (courseSchoolId course) -- join
<*> count [CourseParticipantCourseId ==. cid] -- join <*> count [CourseParticipantCourseId ==. cid] -- join
<*> (case mbAid of -- TODO: Someone please refactor this late-night mess here! <*> (case mbAid of -- TODO: Someone please refactor this late-night mess here!
Nothing -> return False Nothing -> return False
(Just aid) -> do (Just aid) -> do
regL <- getBy (UniqueCourseParticipant cid aid) regL <- getBy (UniqueCourseParticipant cid aid)
return $ isJust regL) return $ isJust regL)
return $ (courseEnt,dependent) return $ (courseEnt,dependent)
let course = entityVal courseEnt let course = entityVal courseEnt
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered (regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerButton $ mbRegistered
defaultLayout $ do defaultLayout $ do
setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|] setTitle $ [shamlet| #{toPathPiece tid} - #{csh}|]
$(widgetFile "course") $(widgetFile "course")
registerButton :: Bool -> Form () registerButton :: Bool -> Form ()
registerButton registered = renderAForm FormStandard $ registerButton registered = renderAForm FormStandard $
pure () <* bootstrapSubmit regMsg pure () <* bootstrapSubmit regMsg
where where
msg = if registered then "Abmelden" else "Anmelden" msg = if registered then "Abmelden" else "Anmelden"
regMsg = msg :: BootstrapSubmit Text regMsg = msg :: BootstrapSubmit Text
postCourseShowR :: TermId -> Text -> Handler Html postCourseShowR :: TermId -> Text -> Handler Html
postCourseShowR tid csh = do postCourseShowR tid csh = do
aid <- requireAuthId aid <- requireAuthId
@ -110,30 +111,30 @@ postCourseShowR tid csh = do
(Entity cid _) <- getBy404 $ CourseTermShort tid csh (Entity cid _) <- getBy404 $ CourseTermShort tid csh
registered <- isJust <$> (getBy $ UniqueCourseParticipant cid aid) registered <- isJust <$> (getBy $ UniqueCourseParticipant cid aid)
return (cid, registered) return (cid, registered)
((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered ((regResult,_), _) <- runFormPost $ identifyForm "registerBtn" $ registerButton registered
case regResult of case regResult of
(FormSuccess _) (FormSuccess _)
| registered -> do | registered -> do
runDB $ deleteBy $ UniqueCourseParticipant cid aid runDB $ deleteBy $ UniqueCourseParticipant cid aid
addMessage "info" "Sie wurden abgemeldet." addMessage "info" "Sie wurden abgemeldet."
| otherwise -> do | otherwise -> do
actTime <- liftIO $ getCurrentTime actTime <- liftIO $ getCurrentTime
regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime
when (isJust regOk) $ addMessage "success" "Erfolgreich angemeldet!" when (isJust regOk) $ addMessage "success" "Erfolgreich angemeldet!"
(_other) -> return () -- TODO check this! (_other) -> return () -- TODO check this!
-- redirect or not?! I guess not, since we want GET now -- redirect or not?! I guess not, since we want GET now
getCourseShowR tid csh getCourseShowR tid csh
getCourseEditR :: Handler Html getCourseEditR :: Handler Html
getCourseEditR = do getCourseEditR = do
-- TODO: Defaults für Semester hier ermitteln und übergeben -- TODO: Defaults für Semester hier ermitteln und übergeben
courseEditHandler Nothing courseEditHandler Nothing
postCourseEditR :: Handler Html postCourseEditR :: Handler Html
postCourseEditR = courseEditHandler Nothing postCourseEditR = courseEditHandler Nothing
getCourseEditExistR :: TermId -> Text -> Handler Html getCourseEditExistR :: TermId -> Text -> Handler Html
getCourseEditExistR tid csh = do getCourseEditExistR tid csh = do
course <- runDB $ getBy $ CourseTermShort tid csh course <- runDB $ getBy $ CourseTermShort tid csh
courseEditHandler course courseEditHandler course
@ -143,28 +144,28 @@ getCourseEditExistIDR cID = do
courseID <- UUID.decrypt cIDKey cID courseID <- UUID.decrypt cIDKey cID
courseEditHandler =<< runDB (getEntity courseID) courseEditHandler =<< runDB (getEntity courseID)
courseEditHandler :: Maybe (Entity Course) -> Handler Html courseEditHandler :: Maybe (Entity Course) -> Handler Html
courseEditHandler course = do courseEditHandler course = do
aid <- requireAuthId aid <- requireAuthId
((result, formWidget), formEnctype) <- runFormPost $ newCourseForm $ courseToForm <$> course ((result, formWidget), formEnctype) <- runFormPost $ newCourseForm $ courseToForm <$> course
action <- lookupPostParam "formaction" action <- lookupPostParam "formaction"
case (result,action) of case (result,action) of
(FormSuccess res, fAct) (FormSuccess res, fAct)
| fAct == formActionDelete | fAct == formActionDelete
, Just cid <- cfCourseId res -> do , Just cid <- cfCourseId res -> do
runDB $ deleteCascade cid -- TODO Sicherheitsabfrage einbauen! runDB $ deleteCascade cid -- TODO Sicherheitsabfrage einbauen!
let cti = toPathPiece $ cfTerm res let cti = toPathPiece $ cfTerm res
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde gelöscht!|] addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde gelöscht!|]
redirect $ CourseListTermR $ cfTerm res redirect $ CourseListTermR $ cfTerm res
| fAct == formActionSave | fAct == formActionSave
, Just cid <- cfCourseId res -> do , Just cid <- cfCourseId res -> do
let tid = cfTerm res let tid = cfTerm res
actTime <- liftIO getCurrentTime actTime <- liftIO getCurrentTime
updateokay <- runDB $ do updateokay <- runDB $ do
exists <- getBy $ CourseTermShort tid $ cfShort res exists <- getBy $ CourseTermShort tid $ cfShort res
let upokay = isNothing exists let upokay = isNothing exists
when upokay $ update cid when upokay $ update cid
[ CourseName =. cfName res [ CourseName =. cfName res
, CourseDescription =. cfDesc res , CourseDescription =. cfDesc res
, CourseLinkExternal =. cfLink res , CourseLinkExternal =. cfLink res
@ -179,17 +180,17 @@ courseEditHandler course = do
] ]
return upokay return upokay
let cti = toPathPiece $ cfTerm res let cti = toPathPiece $ cfTerm res
if updateokay if updateokay
then do then do
addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde geändert. |] addMessage "info" [shamlet| Kurs #{cti}/#{cfShort res} wurde geändert. |]
redirect $ CourseListTermR $ cfTerm res redirect $ CourseListTermR $ cfTerm res
else do else do
addMessage "danger" [shamlet| Kurs #{cti}/#{cfShort res} konnte nicht geändert werden. addMessage "danger" [shamlet| Kurs #{cti}/#{cfShort res} konnte nicht geändert werden.
\ Es gibt bereits einen anderen Kurs mit diesem Kürzel in diesem Semester.|] \ Es gibt bereits einen anderen Kurs mit diesem Kürzel in diesem Semester.|]
| fAct == formActionSave | fAct == formActionSave
, Nothing <- cfCourseId res -> do , Nothing <- cfCourseId res -> do
actTime <- liftIO getCurrentTime actTime <- liftIO getCurrentTime
insertOkay <- runDB $ insertUnique $ Course insertOkay <- runDB $ insertUnique $ Course
{ courseName = cfName res { courseName = cfName res
, courseDescription = cfDesc res , courseDescription = cfDesc res
, courseLinkExternal = cfLink res , courseLinkExternal = cfLink res
@ -204,17 +205,17 @@ courseEditHandler course = do
, courseChanged = actTime , courseChanged = actTime
, courseCreatedBy = aid , courseCreatedBy = aid
, courseChangedBy = aid , courseChangedBy = aid
} }
case insertOkay of case insertOkay of
(Just cid) -> do (Just cid) -> do
runDB $ insert_ $ Lecturer aid cid runDB $ insert_ $ Lecturer aid cid
let cti = toPathPiece $ cfTerm res let cti = toPathPiece $ cfTerm res
addMessage "info" [shamlet|Kurs #{cti}/#{cfShort res} wurde angelegt.|] addMessage "info" [shamlet|Kurs #{cti}/#{cfShort res} wurde angelegt.|]
redirect $ CourseListTermR $ cfTerm res redirect $ CourseListTermR $ cfTerm res
Nothing -> do Nothing -> do
let cti = toPathPiece $ cfTerm res let cti = toPathPiece $ cfTerm res
addMessage "danger" [shamlet|Es gibt bereits einen Kurs #{cfShort res} in Semester #{cti}.|] addMessage "danger" [shamlet|Es gibt bereits einen Kurs #{cfShort res} in Semester #{cti}.|]
(FormFailure _,_) -> addMessage "warning" "Bitte Eingabe korrigieren." (FormFailure _,_) -> addMessage "warning" "Bitte Eingabe korrigieren."
_other -> return () _other -> return ()
let formTitle = "Kurs editieren/anlegen" :: Text let formTitle = "Kurs editieren/anlegen" :: Text
let actionUrl = CourseEditR let actionUrl = CourseEditR
@ -222,28 +223,28 @@ courseEditHandler course = do
defaultLayout $ do defaultLayout $ do
setTitle [shamlet| #{formTitle} |] setTitle [shamlet| #{formTitle} |]
$(widgetFile "formPage") $(widgetFile "formPage")
data CourseForm = CourseForm data CourseForm = CourseForm
{ cfCourseId :: Maybe CourseId -- Maybe CryptoUUIDCourse { cfCourseId :: Maybe CourseId -- Maybe CryptoUUIDCourse
, cfName :: Text , cfName :: Text
, cfDesc :: Maybe Html , cfDesc :: Maybe Html
, cfLink :: Maybe Text , cfLink :: Maybe Text
, cfShort :: Text , cfShort :: Text
, cfTerm :: TermId , cfTerm :: TermId
, cfSchool :: SchoolId , cfSchool :: SchoolId
, cfCapacity :: Maybe Int , cfCapacity :: Maybe Int
, cfHasReg :: Bool , cfHasReg :: Bool
, cfRegFrom :: Maybe UTCTime , cfRegFrom :: Maybe UTCTime
, cfRegTo :: Maybe UTCTime , cfRegTo :: Maybe UTCTime
} }
instance Show CourseForm where instance Show CourseForm where
show cf = T.unpack (cfShort cf) ++ ' ':(show $ cfCourseId cf) show cf = T.unpack (cfShort cf) ++ ' ':(show $ cfCourseId cf)
courseToForm :: Entity Course -> CourseForm courseToForm :: Entity Course -> CourseForm
courseToForm cEntity = CourseForm courseToForm cEntity = CourseForm
{ cfCourseId = Just $ entityKey cEntity { cfCourseId = Just $ entityKey cEntity
, cfName = courseName course , cfName = courseName course
, cfDesc = courseDescription course , cfDesc = courseDescription course
@ -253,26 +254,26 @@ courseToForm cEntity = CourseForm
, cfSchool = courseSchoolId course , cfSchool = courseSchoolId course
, cfCapacity = courseCapacity course , cfCapacity = courseCapacity course
, cfHasReg = courseHasRegistration course , cfHasReg = courseHasRegistration course
, cfRegFrom = courseRegisterFrom course , cfRegFrom = courseRegisterFrom course
, cfRegTo = courseRegisterTo course , cfRegTo = courseRegisterTo course
} }
where where
course = entityVal cEntity course = entityVal cEntity
newCourseForm :: Maybe CourseForm -> Form CourseForm newCourseForm :: Maybe CourseForm -> Form CourseForm
newCourseForm template = identForm FIDcourse $ \html -> do newCourseForm template = identForm FIDcourse $ \html -> do
-- mopt hiddenField -- mopt hiddenField
-- cidKey <- getsYesod appCryptoIDKey -- cidKey <- getsYesod appCryptoIDKey
-- courseId <- runMaybeT $ do -- courseId <- runMaybeT $ do
-- cid <- cfCourseId template -- cid <- cfCourseId template
-- UUID.encrypt cidKey cid -- UUID.encrypt cidKey cid
(result, widget) <- flip (renderAForm FormStandard) html $ CourseForm (result, widget) <- flip (renderAForm FormStandard) html $ CourseForm
-- <$> pure cid -- $ join $ cfCourseId <$> template -- why doesnt this work? -- <$> pure cid -- $ join $ cfCourseId <$> template -- why doesnt this work?
<$> aopt hiddenField "KursId" (cfCourseId <$> template) <$> aopt hiddenField "KursId" (cfCourseId <$> template)
<*> areq textField (fsb "Name") (cfName <$> template) <*> areq textField (fsb "Name") (cfName <$> template)
<*> aopt htmlField (fsb "Beschreibung") (cfDesc <$> template) <*> aopt htmlField (fsb "Beschreibung") (cfDesc <$> template)
<*> aopt urlField (fsb "Homepage") (cfLink <$> template) <*> aopt urlField (fsb "Homepage") (cfLink <$> template)
<*> areq textField (fsb "Kürzel" <*> areq textField (fsb "Kürzel"
-- & addAttr "disabled" "disabled" -- & addAttr "disabled" "disabled"
& setTooltip "Muss innerhalb des Semesters eindeutig sein") & setTooltip "Muss innerhalb des Semesters eindeutig sein")
(cfShort <$> template) (cfShort <$> template)
@ -282,9 +283,9 @@ newCourseForm template = identForm FIDcourse $ \html -> do
<*> areq checkBoxField (fsb "Anmeldung") (cfHasReg <$> template) <*> areq checkBoxField (fsb "Anmeldung") (cfHasReg <$> template)
<*> aopt utcTimeField (fsb "Anmeldung von:") (cfRegFrom <$> template) <*> aopt utcTimeField (fsb "Anmeldung von:") (cfRegFrom <$> template)
<*> aopt utcTimeField (fsb "Anmeldung bis:") (cfRegTo <$> template) <*> aopt utcTimeField (fsb "Anmeldung bis:") (cfRegTo <$> template)
-- <* bootstrapSubmit (bsSubmit (show cid)) -- <* bootstrapSubmit (bsSubmit (show cid))
return $ case result of return $ case result of
FormSuccess courseResult FormSuccess courseResult
| errorMsgs <- validateCourse courseResult | errorMsgs <- validateCourse courseResult
, not $ null errorMsgs -> , not $ null errorMsgs ->
(FormFailure errorMsgs, (FormFailure errorMsgs,
@ -293,18 +294,18 @@ newCourseForm template = identForm FIDcourse $ \html -> do
<h4> Fehler: <h4> Fehler:
<ul> <ul>
$forall errmsg <- errorMsgs $forall errmsg <- errorMsgs
<li> #{errmsg} <li> #{errmsg}
^{widget} ^{widget}
|] |]
) )
_ -> (result, widget) _ -> (result, widget)
-- where -- where
-- cid :: Maybe CourseId -- cid :: Maybe CourseId
-- cid = join $ cfCourseId <$> template -- cid = join $ cfCourseId <$> template
validateCourse :: CourseForm -> [Text] validateCourse :: CourseForm -> [Text]
validateCourse (CourseForm{..}) = validateCourse (CourseForm{..}) =
[ msg | (False, msg) <- [ msg | (False, msg) <-
[ [
( cfRegFrom <= cfRegTo ( cfRegFrom <= cfRegTo
@ -324,5 +325,3 @@ validateCourse (CourseForm{..}) =
, "Anmeldungen aktivieren oder Anmeldezeitraum löschen" , "Anmeldungen aktivieren oder Anmeldezeitraum löschen"
) )
] ] ] ]

View File

@ -9,13 +9,13 @@
module Handler.Home where module Handler.Home where
import Import import Import
import Handler.Utils import Handler.Utils
-- import Data.Time -- import Data.Time
-- import qualified Data.Text as T -- import qualified Data.Text as T
-- import Yesod.Form.Bootstrap3 -- import Yesod.Form.Bootstrap3
import Web.PathPieces (showToPathPiece, readFromPathPiece) import Web.PathPieces (showToPathPiece, readFromPathPiece)
-- import Colonnade -- import Colonnade
@ -30,29 +30,28 @@ data CreateButton = CreateMath | CreateInf -- Dummy for Example
instance PathPiece CreateButton where -- for displaying the button only, not really for paths instance PathPiece CreateButton where -- for displaying the button only, not really for paths
toPathPiece = showToPathPiece toPathPiece = showToPathPiece
fromPathPiece = readFromPathPiece fromPathPiece = readFromPathPiece
instance Button CreateButton where instance Button CreateButton where
label CreateMath = [whamlet|Ma<i>thema</i>tik|] label CreateMath = [whamlet|Ma<i>thema</i>tik|]
label CreateInf = "Informatik" label CreateInf = "Informatik"
cssClass CreateMath = BCInfo cssClass CreateMath = BCInfo
cssClass CreateInf = BCPrimary cssClass CreateInf = BCPrimary
-- END Button needed here -- END Button needed here
getHomeR :: Handler Html getHomeR :: Handler Html
getHomeR = do getHomeR = do
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton) (btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton)
defaultLayout $ do defaultLayout $ do
setTitle "Willkommen zum ReWorX Test!" setTitle "Willkommen zum ReWorX Test!"
$(widgetFile "home") $(widgetFile "home")
postHomeR :: Handler Html postHomeR :: Handler Html
postHomeR = do postHomeR = do
((btnResult,_), _) <- runFormPost $ buttonForm ((btnResult,_), _) <- runFormPost $ buttonForm
case btnResult of case btnResult of
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt" (FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
(FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt" (FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt"
_other -> return () _other -> return ()
getHomeR getHomeR

View File

@ -9,11 +9,11 @@
module Handler.Term where module Handler.Term where
import Import import Import
import Handler.Utils import Handler.Utils
import qualified Data.Text as T import qualified Data.Text as T
import Yesod.Form.Bootstrap3 import Yesod.Form.Bootstrap3
import Colonnade hiding (bool) import Colonnade hiding (bool)
import Yesod.Colonnade import Yesod.Colonnade
@ -27,47 +27,47 @@ getTermShowR = do
-- term <- runDB $ E.select . E.from $ \(term) -> do -- term <- runDB $ E.select . E.from $ \(term) -> do
-- E.orderBy [E.desc $ term E.^. TermStart ] -- E.orderBy [E.desc $ term E.^. TermStart ]
-- return term -- return term
-- --
let let
termData = E.from $ \term -> do termData = E.from $ \term -> do
E.orderBy [E.desc $ term E.^. TermStart ] E.orderBy [E.desc $ term E.^. TermStart ]
let courseCount :: E.SqlExpr (E.Value Int) let courseCount :: E.SqlExpr (E.Value Int)
courseCount = E.sub_select . E.from $ \course -> do courseCount = E.sub_select . E.from $ \course -> do
E.where_ $ term E.^. TermId E.==. course E.^. CourseTermId E.where_ $ term E.^. TermId E.==. course E.^. CourseTermId
return E.countRows return E.countRows
return (term, courseCount) return (term, courseCount)
selectRep $ do selectRep $ do
provideRep $ toJSON . map fst <$> runDB (E.select termData) provideRep $ toJSON . map fst <$> runDB (E.select termData)
provideRep $ do provideRep $ do
let colonnadeTerms = mconcat let colonnadeTerms = mconcat
[ headed "Kürzel" $ \(Entity tid Term{..},_) -> cell $ do [ headed "Kürzel" $ \(Entity tid Term{..},_) -> cell $ do
-- Scrap this if to slow, create term edit page instead -- Scrap this if to slow, create term edit page instead
adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False
[whamlet| [whamlet|
$if adminLink == Authorized $if adminLink == Authorized
<a href=@{TermEditExistR tid}> <a href=@{TermEditExistR tid}>
#{termToText termName} #{termToText termName}
$else $else
#{termToText termName} #{termToText termName}
|] |]
, headed "Beginn Vorlesungen" $ \(Entity _ Term{..},_) -> , headed "Beginn Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureStart stringCell $ formatTimeGerWD termLectureStart
, headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) -> , headed "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureEnd stringCell $ formatTimeGerWD termLectureEnd
, headed "Aktiv" $ \(Entity _ Term{..},_) -> , headed "Aktiv" $ \(Entity _ Term{..},_) ->
textCell $ bool "" tickmark termActive textCell $ bool "" tickmark termActive
, headed "Kursliste" $ \(Entity tid Term{..}, E.Value numCourses) -> , headed "Kursliste" $ \(Entity tid Term{..}, E.Value numCourses) ->
cell [whamlet| cell [whamlet|
<a href=@{CourseListTermR tid}> <a href=@{CourseListTermR tid}>
#{show numCourses} Kurse #{show numCourses} Kurse
|] |]
, headed "Semesteranfang" $ \(Entity _ Term{..},_) -> , headed "Semesteranfang" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termStart stringCell $ formatTimeGerWD termStart
, headed "Semesterende" $ \(Entity _ Term{..},_) -> , headed "Semesterende" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termEnd stringCell $ formatTimeGerWD termEnd
, headed "Feiertage im Semester" $ \(Entity _ Term{..},_) -> , headed "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays
] ]
table <- dbTable def $ DBTable table <- dbTable def $ DBTable
{ dbtSQLQuery = termData { dbtSQLQuery = termData
, dbtColonnade = colonnadeTerms , dbtColonnade = colonnadeTerms
@ -79,30 +79,29 @@ getTermShowR = do
setTitle "Freigeschaltete Semester" setTitle "Freigeschaltete Semester"
table table
getTermEditR :: Handler Html getTermEditR :: Handler Html
getTermEditR = do getTermEditR = do
-- TODO: Defaults für Semester hier ermitteln und übergeben -- TODO: Defaults für Semester hier ermitteln und übergeben
termEditHandler Nothing termEditHandler Nothing
postTermEditR :: Handler Html postTermEditR :: Handler Html
postTermEditR = termEditHandler Nothing postTermEditR = termEditHandler Nothing
getTermEditExistR :: TermId -> Handler Html getTermEditExistR :: TermId -> Handler Html
getTermEditExistR tid = do getTermEditExistR tid = do
term <- runDB $ get tid term <- runDB $ get tid
termEditHandler term termEditHandler term
termEditHandler :: Maybe Term -> Handler Html termEditHandler :: Maybe Term -> Handler Html
termEditHandler term = do termEditHandler term = do
((result, formWidget), formEnctype) <- runFormPost $ newTermForm term ((result, formWidget), formEnctype) <- runFormPost $ newTermForm term
case result of case result of
(FormSuccess res) -> do (FormSuccess res) -> do
-- term <- runDB $ get $ TermKey termName -- term <- runDB $ get $ TermKey termName
runDB $ repsert (TermKey $ termName res) res runDB $ repsert (TermKey $ termName res) res
let tid = termToText $ termName res let tid = termToText $ termName res
let msg = "Semester " `T.append` tid `T.append` " erfolgreich editiert." let msg = "Semester " `T.append` tid `T.append` " erfolgreich editiert."
addMessage "success" [shamlet| #{msg} |] addMessage "success" [shamlet| #{msg} |]
redirect TermShowR redirect TermShowR
(FormMissing ) -> return () (FormMissing ) -> return ()
@ -112,7 +111,7 @@ termEditHandler term = do
defaultLayout $ do defaultLayout $ do
setTitle [shamlet| #{formTitle} |] setTitle [shamlet| #{formTitle} |]
$(widgetFile "formPage") $(widgetFile "formPage")
newTermForm :: Maybe Term -> Form Term newTermForm :: Maybe Term -> Form Term
newTermForm template html = do newTermForm template html = do
(result, widget) <- flip (renderAForm FormStandard) html $ Term (result, widget) <- flip (renderAForm FormStandard) html $ Term
@ -124,8 +123,8 @@ newTermForm template html = do
<*> areq dayField (bfs ("Ende Vorlesungen" :: Text)) (termLectureEnd <$> template) <*> areq dayField (bfs ("Ende Vorlesungen" :: Text)) (termLectureEnd <$> template)
<*> areq checkBoxField (bfs ("Aktiv" :: Text)) (termActive <$> template) <*> areq checkBoxField (bfs ("Aktiv" :: Text)) (termActive <$> template)
<* submitButton <* submitButton
return $ case result of return $ case result of
FormSuccess termResult FormSuccess termResult
| errorMsgs <- validateTerm termResult | errorMsgs <- validateTerm termResult
, not $ null errorMsgs -> , not $ null errorMsgs ->
(FormFailure errorMsgs, (FormFailure errorMsgs,
@ -134,13 +133,13 @@ newTermForm template html = do
<h4> Fehler: <h4> Fehler:
<ul> <ul>
$forall errmsg <- errorMsgs $forall errmsg <- errorMsgs
<li> #{errmsg} <li> #{errmsg}
^{widget} ^{widget}
|] |]
) )
_ -> (result, widget) _ -> (result, widget)
{- {-
where where
set :: Text -> FieldSettings site set :: Text -> FieldSettings site
set = bfs set = bfs
-} -}

View File

@ -41,5 +41,5 @@ getUsersR = do
-- ++ map (\school -> headed (text2widget $ schoolName $ entityVal school) (\u -> "xx")) schools -- ++ map (\school -> headed (text2widget $ schoolName $ entityVal school) (\u -> "xx")) schools
defaultLayout $ do defaultLayout $ do
setTitle "Comprehensive User List" setTitle "Comprehensive User List"
let userList = encodeWidgetTable tableDefault colonnadeUsers users let userList = encodeWidgetTable tableSortable colonnadeUsers users
$(widgetFile "users") $(widgetFile "users")

View File

@ -18,6 +18,7 @@ import Handler.Utils.Table.Pagination as Handler.Utils
import Handler.Utils.Zip as Handler.Utils import Handler.Utils.Zip as Handler.Utils
import Handler.Utils.Rating as Handler.Utils import Handler.Utils.Rating as Handler.Utils
import Handler.Utils.Submission as Handler.Utils import Handler.Utils.Submission as Handler.Utils
import Handler.Utils.Templates as Handler.Utils
import Text.Blaze (Markup, ToMarkup) import Text.Blaze (Markup, ToMarkup)

View File

@ -8,7 +8,7 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
module Handler.Utils.Form where module Handler.Utils.Form where
import Import import Import
import qualified Data.Char as Char import qualified Data.Char as Char
@ -21,7 +21,7 @@ import qualified Data.Foldable as Foldable
import qualified Data.Text as T import qualified Data.Text as T
-- import Yesod.Form.Types -- import Yesod.Form.Types
import Yesod.Form.Functions (parseHelper) import Yesod.Form.Functions (parseHelper)
import Yesod.Form.Bootstrap3 import Yesod.Form.Bootstrap3
import qualified Text.Blaze.Internal as Blaze (null) import qualified Text.Blaze.Internal as Blaze (null)
@ -35,7 +35,7 @@ data FormIdentifier = FIDcourse | FIDsheet
deriving (Enum, Eq, Ord, Bounded, Read, Show) deriving (Enum, Eq, Ord, Bounded, Read, Show)
identForm :: FormIdentifier -> Form a -> Form a identForm :: FormIdentifier -> Form a -> Form a
identForm fid = identifyForm (T.pack $ show fid) identForm fid = identifyForm (T.pack $ show fid)
------------------- -------------------
@ -48,7 +48,7 @@ data FormLayout = FormStandard
renderAForm :: Monad m => FormLayout -> FormRender m a renderAForm :: Monad m => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do renderAForm formLayout aform fragment = do
(res, (($ []) -> views)) <- aFormToForm aform (res, (($ []) -> views)) <- aFormToForm aform
let widget = $(widgetFile "form") let widget = $(widgetFile "widgets/form")
return (res, widget) return (res, widget)
---------------------------- ----------------------------
@ -67,13 +67,13 @@ class (Enum a, Bounded a, Ord a, PathPiece a) => Button a where
cssClass :: a -> ButtonCssClass cssClass :: a -> ButtonCssClass
cssClass _ = BCDefault cssClass _ = BCDefault
{- Abort is not useful (press Back instead); Delete should be different: {- Abort is not useful (press Back instead); Delete should be different:
data StandardButton = BtnDelete | BtnAbort | BtnSave data StandardButton = BtnDelete | BtnAbort | BtnSave
deriving (Enum, Eq, Ord, Bounded, Read, Show) deriving (Enum, Eq, Ord, Bounded, Read, Show)
instance PathPiece StandardButton where -- for displaying the button only, not really for paths instance PathPiece StandardButton where -- for displaying the button only, not really for paths
toPathPiece = showToPathPiece toPathPiece = showToPathPiece
fromPathPiece = readFromPathPiece fromPathPiece = readFromPathPiece
@ -82,7 +82,7 @@ instance Button StandardButton where
label BtnDelete = "Löschen" label BtnDelete = "Löschen"
label BtnAbort = "Abbrechen" label BtnAbort = "Abbrechen"
label BtnSave = "Speichern" label BtnSave = "Speichern"
cssClass BtnDelete = BCWarning cssClass BtnDelete = BCWarning
cssClass BtnAbort = BCDefault cssClass BtnAbort = BCDefault
cssClass BtnSave = BCPrimary cssClass BtnSave = BCPrimary
@ -99,45 +99,45 @@ instance Button SubmitButton where
label BtnSubmit = "Submit" label BtnSubmit = "Submit"
cssClass BtnSubmit = BCPrimary cssClass BtnSubmit = BCPrimary
-- -- Looks like a button, but is just a link (e.g. for create course, etc.) -- -- Looks like a button, but is just a link (e.g. for create course, etc.)
-- data LinkButton = LinkButton (Route UniWorX) -- data LinkButton = LinkButton (Route UniWorX)
-- deriving (Enum, Eq, Ord, Bounded, Read, Show) -- deriving (Enum, Eq, Ord, Bounded, Read, Show)
-- --
-- instance PathPiece LinkButton where -- instance PathPiece LinkButton where
-- LinkButton route = ??? -- LinkButton route = ???
linkButton :: Widget -> ButtonCssClass -> Route UniWorX -> Widget linkButton :: Widget -> ButtonCssClass -> Route UniWorX -> Widget
linkButton lbl cls url = [whamlet| <a href=@{url} .btn .#{bcc2txt cls} role=button>^{lbl} |] linkButton lbl cls url = [whamlet| <a href=@{url} .btn .#{bcc2txt cls} role=button>^{lbl} |]
-- [whamlet| -- [whamlet|
-- <form method=post action=@{url}> -- <form method=post action=@{url}>
-- <input type="hidden" name="_formid" value="identify-linkButton"> -- <input type="hidden" name="_formid" value="identify-linkButton">
-- <button .btn .#{bcc2txt cls} type=submit value="Link to @{url}">^{lbl} -- <button .btn .#{bcc2txt cls} type=submit value="Link to @{url}">^{lbl}
-- |] -- |]
-- <input .btn .#{bcc2txt cls} type="submit" value=^{lbl}> -- <input .btn .#{bcc2txt cls} type="submit" value=^{lbl}>
buttonField :: Button a => a -> Field Handler a buttonField :: Button a => a -> Field Handler a
buttonField btn = Field {fieldParse, fieldView, fieldEnctype} buttonField btn = Field {fieldParse, fieldView, fieldEnctype}
where where
fieldEnctype = UrlEncoded fieldEnctype = UrlEncoded
fieldView fid name attrs _val _ = fieldView fid name attrs _val _ =
[whamlet| [whamlet|
<button .btn .#{bcc2txt $ cssClass btn} type=submit name=#{name} value=#{toPathPiece btn} *{attrs} ##{fid}>^{label btn} <button .btn .#{bcc2txt $ cssClass btn} type=submit name=#{name} value=#{toPathPiece btn} *{attrs} ##{fid}>^{label btn}
|] |]
fieldParse [] _ = return $ Right Nothing fieldParse [] _ = return $ Right Nothing
fieldParse [str] _ fieldParse [str] _
| str == toPathPiece btn = return $ Right $ Just btn | str == toPathPiece btn = return $ Right $ Just btn
| otherwise = return $ Left "Wrong button value" | otherwise = return $ Left "Wrong button value"
fieldParse _ _ = return $ Left "Multiple button values" fieldParse _ _ = return $ Left "Multiple button values"
combinedButtonField :: Button a => [a] -> AForm Handler [Maybe a] combinedButtonField :: Button a => [a] -> AForm Handler [Maybe a]
combinedButtonField btns = traverse b2f btns combinedButtonField btns = traverse b2f btns
where where
b2f b = aopt (buttonField b) "" Nothing b2f b = aopt (buttonField b) "" Nothing
submitButton :: AForm Handler () submitButton :: AForm Handler ()
submitButton = void $ combinedButtonField [BtnSubmit] submitButton = void $ combinedButtonField [BtnSubmit]
@ -146,10 +146,10 @@ submitButton = void $ combinedButtonField [BtnSubmit]
combinedButtonField :: Button a => [a] -> Form m -> Form (a,m) combinedButtonField :: Button a => [a] -> Form m -> Form (a,m)
combinedButtonField btns inner csrf = do combinedButtonField btns inner csrf = do
buttonIdent <- newFormIdent buttonIdent <- newFormIdent
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
(results, btnViews) <- unzip <$> mapM button [minBound..maxBound] (results, btnViews) <- unzip <$> mapM button [minBound..maxBound]
(innerRes,innerWdgt) <- inner (innerRes,innerWdgt) <- inner
let widget = do let widget = do
[whamlet| [whamlet|
#{csrf} #{csrf}
^{innerWdgt} ^{innerWdgt}
@ -171,14 +171,14 @@ combinedButtonField btns inner csrf = do
accResult' _ x@(FormSuccess _) = x --SJ: Is this safe? Shouldn't Failure override Success? accResult' _ x@(FormSuccess _) = x --SJ: Is this safe? Shouldn't Failure override Success?
accResult' (FormSuccess Nothing) x = x accResult' (FormSuccess Nothing) x = x
accResult' FormMissing _ = FormMissing accResult' FormMissing _ = FormMissing
accResult' (FormFailure errs) _ = FormFailure errs accResult' (FormFailure errs) _ = FormFailure errs
-} -}
-- buttonForm :: Button a => Markup -> MForm (HandlerT UniWorX IO) (FormResult a, (WidgetT UniWorX IO ())) -- buttonForm :: Button a => Markup -> MForm (HandlerT UniWorX IO) (FormResult a, (WidgetT UniWorX IO ()))
buttonForm :: (Button a) => Form a buttonForm :: (Button a) => Form a
buttonForm csrf = do buttonForm csrf = do
buttonIdent <- newFormIdent buttonIdent <- newFormIdent
let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing let button b = mopt (buttonField b) ("n/a"{ fsName = Just buttonIdent }) Nothing
(results, btnViews) <- unzip <$> mapM button [minBound..maxBound] (results, btnViews) <- unzip <$> mapM button [minBound..maxBound]
let widget = do let widget = do
[whamlet| [whamlet|
@ -199,7 +199,7 @@ buttonForm csrf = do
accResult' FormMissing _ = FormMissing accResult' FormMissing _ = FormMissing
accResult' (FormFailure errs) _ = FormFailure errs accResult' (FormFailure errs) _ = FormFailure errs
--------------------------------------- ---------------------------------------
-- Buttons (old version, deprecated) -- -- Buttons (old version, deprecated) --
--------------------------------------- ---------------------------------------
@ -235,8 +235,8 @@ postButtonForm lblId = identifyForm lblId buttonF
buttonF = renderAForm FormStandard $ pure () <* bootstrapSubmit bProps buttonF = renderAForm FormStandard $ pure () <* bootstrapSubmit bProps
bProps :: BootstrapSubmit Text bProps :: BootstrapSubmit Text
bProps = fromString $ unpack lblId bProps = fromString $ unpack lblId
------------ ------------
-- Fields -- -- Fields --
------------ ------------
@ -273,7 +273,7 @@ sheetTypeAFormReq d (Just (Normal p)) =
utcTimeField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m UTCTime utcTimeField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m UTCTime
-- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing) -- StackOverflow: dayToUTC <$> (areq (jqueryDayField def {...}) settings Nothing)
utcTimeField = Field utcTimeField = Field
{ fieldParse = parseHelper $ readTime { fieldParse = parseHelper $ readTime
, fieldView = \theId name attrs val isReq -> , fieldView = \theId name attrs val isReq ->
[whamlet| [whamlet|
@ -282,42 +282,42 @@ utcTimeField = Field
|] |]
, fieldEnctype = UrlEncoded , fieldEnctype = UrlEncoded
} }
where where
fieldTimeFormat :: String fieldTimeFormat :: String
--fieldTimeFormat = "%e.%m.%y %k:%M" --fieldTimeFormat = "%e.%m.%y %k:%M"
fieldTimeFormat = "%Y-%m-%eT%H:%M" fieldTimeFormat = "%Y-%m-%eT%H:%M"
readTime :: Text -> Either FormMessage UTCTime readTime :: Text -> Either FormMessage UTCTime
readTime t = readTime t =
case parseTimeM True germanTimeLocale fieldTimeFormat (T.unpack t) of case parseTimeM True germanTimeLocale fieldTimeFormat (T.unpack t) of
(Just time) -> Right time (Just time) -> Right time
Nothing -> Left $ MsgInvalidEntry $ "Datum/Zeit Format: tt.mm.yy hh:mm " ++ t Nothing -> Left $ MsgInvalidEntry $ "Datum/Zeit Format: tt.mm.yy hh:mm " ++ t
showTime :: UTCTime -> Text showTime :: UTCTime -> Text
showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat) showTime = fromString . (formatTime germanTimeLocale fieldTimeFormat)
fsb :: Text -> FieldSettings site fsb :: Text -> FieldSettings site
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
addAttr :: Text -> Text -> FieldSettings site -> FieldSettings site addAttr :: Text -> Text -> FieldSettings site -> FieldSettings site
addAttr attr valu fs = fs { fsAttrs=newAttrs (fsAttrs fs) } addAttr attr valu fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
where where
newAttrs :: [(Text,Text)] -> [(Text,Text)] newAttrs :: [(Text,Text)] -> [(Text,Text)]
newAttrs [] = [(attr,valu)] newAttrs [] = [(attr,valu)]
newAttrs (p@(a,v):t) newAttrs (p@(a,v):t)
| attr==a = (a,T.append valu $ cons ' ' v):t | attr==a = (a,T.append valu $ cons ' ' v):t
| otherwise = p:(newAttrs t) | otherwise = p:(newAttrs t)
addAttrs :: Text -> [Text] -> FieldSettings site -> FieldSettings site addAttrs :: Text -> [Text] -> FieldSettings site -> FieldSettings site
addAttrs attr valus fs = fs { fsAttrs=newAttrs (fsAttrs fs) } addAttrs attr valus fs = fs { fsAttrs=newAttrs (fsAttrs fs) }
where where
newAttrs :: [(Text,Text)] -> [(Text,Text)] newAttrs :: [(Text,Text)] -> [(Text,Text)]
newAttrs [] = [(attr,T.intercalate " " valus)] newAttrs [] = [(attr,T.intercalate " " valus)]
newAttrs (p@(a,v):t) newAttrs (p@(a,v):t)
| attr==a = (a,T.intercalate " " (v:valus)):t | attr==a = (a,T.intercalate " " (v:valus)):t
| otherwise = p:(newAttrs t) | otherwise = p:(newAttrs t)
addClass :: Text -> FieldSettings site -> FieldSettings site addClass :: Text -> FieldSettings site -> FieldSettings site
addClass = addAttr "class" addClass = addAttr "class"
@ -334,7 +334,7 @@ addIdClass :: Text -> Text -> FieldSettings site -> FieldSettings site
addIdClass gId gClass fs = fs { fsId= Just gId, fsAttrs=("class",gClass):(fsAttrs fs) } addIdClass gId gClass fs = fs { fsId= Just gId, fsAttrs=("class",gClass):(fsAttrs fs) }
setClass :: FieldSettings site -> Text -> FieldSettings site -- deprecated setClass :: FieldSettings site -> Text -> FieldSettings site -- deprecated
setClass fs c = fs { fsAttrs=("class",c):(fsAttrs fs) } setClass fs c = fs { fsAttrs=("class",c):(fsAttrs fs) }
setNameClass :: FieldSettings site -> Text -> Text -> FieldSettings site -- deprecated setNameClass :: FieldSettings site -> Text -> Text -> FieldSettings site -- deprecated
@ -344,4 +344,3 @@ setTooltip :: String -> FieldSettings site -> FieldSettings site
setTooltip tt fs setTooltip tt fs
| null tt = fs { fsTooltip = Nothing } | null tt = fs { fsTooltip = Nothing }
| otherwise = fs { fsTooltip = Just $ fromString tt } | otherwise = fs { fsTooltip = Just $ fromString tt }

View File

@ -23,12 +23,15 @@ import Data.Either
-- Table design -- Table design
tableDefault :: Attribute tableDefault :: Attribute
tableDefault = customAttribute "class" "table table-striped table-hover" tableDefault = customAttribute "class" "table table-striped table-hover"
tableSortable :: Attribute
tableSortable = customAttribute "class" "js-sortable"
-- Colonnade Tools -- Colonnade Tools
numberColonnade :: (IsString c) => Colonnade Headed Int c numberColonnade :: (IsString c) => Colonnade Headed Int c
numberColonnade = headed "Nr" (fromString.show) numberColonnade = headed "Nr" (fromString.show)
pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c
pairColonnade a b = mconcat [ lmap fst a, lmap snd b] pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
@ -39,8 +42,8 @@ encodeHeadedWidgetTableNumbered attrs colo tdata =
encodeWidgetTable attrs (mconcat [numberCol, lmap snd colo]) (zip [1..] tdata) encodeWidgetTable attrs (mconcat [numberCol, lmap snd colo]) (zip [1..] tdata)
where where
numberCol :: Colonnade Headed (Int,a) (WidgetT site IO ()) numberCol :: Colonnade Headed (Int,a) (WidgetT site IO ())
numberCol = headed "Nr" (fromString.show.fst) numberCol = headed "Nr" (fromString.show.fst)
headedRowSelector :: ( PathPiece b headedRowSelector :: ( PathPiece b
, Eq b , Eq b
) )
@ -74,7 +77,7 @@ headedRowSelector toExternal fromExternal attrs colonnade tdata = do
selectionIdent <- newFormIdent selectionIdent <- newFormIdent
(selectionResults, selectionBoxes) <- fmap unzip . forM externalIds $ \ident -> mopt (checkbox ident) ("" { fsName = Just selectionIdent }) Nothing (selectionResults, selectionBoxes) <- fmap unzip . forM externalIds $ \ident -> mopt (checkbox ident) ("" { fsName = Just selectionIdent }) Nothing
let let
selColonnade :: Colonnade Headed Int (Cell UniWorX) selColonnade :: Colonnade Headed Int (Cell UniWorX)
selColonnade = headed "Markiert" $ cell . fvInput . (selectionBoxes !!) selColonnade = headed "Markiert" $ cell . fvInput . (selectionBoxes !!)

View File

@ -0,0 +1,8 @@
{-# LANGUAGE NoImplicitPrelude, TemplateHaskell #-}
module Handler.Utils.Templates where
import Import.NoFoundation
lipsum :: WidgetT site IO ()
lipsum = $(widgetFile "widgets/lipsum")

View File

@ -16,3 +16,6 @@ import Data.Fixed as Import
import CryptoID as Import import CryptoID as Import
import Data.UUID as Import (UUID) import Data.UUID as Import (UUID)
import Text.Lucius as Import

7022
static/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

9
static/css/fonts.css Normal file
View File

@ -0,0 +1,9 @@
@font-face {
font-family: 'Glyphicons Halflings';
src: url('../fonts/glyphicons-halflings-regular.eot');
src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),
url('../fonts/glyphicons-halflings-regular.woff') format('woff'),
url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),
url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
}

30
static/css/icons.css Normal file
View File

@ -0,0 +1,30 @@
.glyphicon {
display: inline-block;
width: 40px;
height: 40px;
line-height: 40px;
}
.glyphicon::before {
position: absolute;
left: 4px;
margin: 0 13px;
font-family: 'Glyphicons Halflings';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.glyphicon--home::before {
content: '\e021';
}
.glyphicon--book::before {
content: '\e043';
}
.glyphicon--user::before {
content: '\e008';
}
.glyphicon--login::before {
content: '\e161';
}
.glyphicon--logout::before {
content: '\e163';
}

View File

@ -1,38 +1,34 @@
<div .masthead> <div .course-header>
<div .container> <div .course-header__info>
<div .row> <table>
<h1 .header> <tr>
#{courseName course} <th>Teilnehmer
$maybe school <- schoolMB <td>
<h4> #{participants}
#{schoolName school} $maybe capacity <- courseCapacity course
\ von #{capacity}
<tr>
<th>Anmeldezeitraum
<td>
$maybe regFrom <- courseRegisterFrom course
#{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo}
<div>
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}>
^{regWidget}
<div .course-header__title>
<h1>#{courseName course}
$maybe school <- schoolMB
<h4>#{schoolName school}
<div .container> <div .container>
<div .bs-docs-section> $maybe descr <- courseDescription course
<div .row> <h2 #description>Beschreibung
<div .col-lg-12> <p> #{descr}
<div .page-header> $maybe link <- courseLinkExternal course
<h4 #linl>Homepage
$maybe descr <- courseDescription course <a href=#{link}>#{link}
<h2 #description>Beschreibung
<p> #{descr}
$maybe link <- courseLinkExternal course
<h4 #linl>Homepage
<a href=#{link}>#{link}
<div .row>
<div .col-lg-12>
<h4>Teilnehmer
#{participants}
$maybe capacity <- courseCapacity course
\ von #{capacity}
<br>
$maybe regFrom <- courseRegisterFrom course
Anmeldezeitraum: #{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo}
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}>
^{regWidget}
<hr>

19
templates/course.lucius Normal file
View File

@ -0,0 +1,19 @@
.course-header {
/*display: flex;
flex-direction: row;
justify-content: space-between;*/
}
.course-header__title {
align-self: baseline;
}
.course-header__info {
border: 1px solid var(--greybase);
padding: 13px;
align-self: center;
float: right;
}
.course-header__info table {
margin: 0;
}

6
templates/courses.hamlet Normal file
View File

@ -0,0 +1,6 @@
<div .container>
<h1>Kursübersicht für Semester #{termToText $ unTermKey tidini}
^{coursesTable}
<div .container>
<a href=@{CourseEditR}>Neuen Kurs anlegen

View File

@ -19,12 +19,7 @@ $newline never
\<!--[if lt IE 9]> \<!--[if lt IE 9]>
\<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script> \<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
\<![endif]--> \<![endif]-->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.js">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.0.3/js.cookie.min.js"> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.0.3/js.cookie.min.js">
\<!-- Bootstrap-3.3.7 compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous">
<script> <script>
/* The `defaultCsrfMiddleware` Middleware added in Foundation.hs adds a CSRF token to the request cookies. */ /* The `defaultCsrfMiddleware` Middleware added in Foundation.hs adds a CSRF token to the request cookies. */
/* AJAX requests should add that token to a header to be validated by the server. */ /* AJAX requests should add that token to a header to be validated by the server. */
@ -43,8 +38,6 @@ $newline never
\ }); \ });
} }
<script>
document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/,'js');
<body> <body>
^{pageBody pc} ^{pageBody pc}

View File

@ -1,60 +1,17 @@
<!-- navigation -->
^{navbar}
<!-- Static navbar --> <div .main>
<nav .navbar.navbar-default.navbar-static-top>
<div .container>
<div .navbar-header>
<button type="button" .navbar-toggle.collapsed data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<div #navbar .collapse.navbar-collapse> <!-- secondary navigation at the side -->
<ul .nav.navbar-nav> ^{asidenav}
$forall menuType <- menuTypes
$case menuType
$of NavbarLeft (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of NavbarExtra (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of _
<ul .nav.navbar-nav.navbar-right> <div .main__content>
$forall menuType <- menuTypes
$case menuType
$of NavbarRight (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of _
<!-- Page Contents -->
<div .container>
$if not $ Just HomeR == mcurrentRoute
<ul .breadcrumb>
$forall bc <- parents
<li>
<a href="@{fst bc}">#{snd bc}
<li .active>#{title}
<!-- alerts -->
$forall (status, msg) <- mmsgs $forall (status, msg) <- mmsgs
$with status2 <- bool status "info" (status == "") $with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}">#{msg} <div class="alert alert-#{status2}">#{msg}
<!-- actual content -->
$if (Just HomeR == mcurrentRoute)
^{widget} ^{widget}
$else
<div .container>
<div .row>
<div .col-md-12>
^{widget}
<!-- Footer -->
<footer .footer>
<div .container>
<p .text-muted>
#{appCopyright $ appSettings master}

View File

@ -1,73 +1,200 @@
.masthead, :root {
.navbar { /* THEME 1 */
background-color: rgb(27, 28, 29); --base00: #72a85b;
} --base-bg-color: #1d1c1d;
--base-font-color: #fff;
.navbar-default .navbar-nav > .active > a { --sec-font-color: #fff;
background-color: transparent; --box-bg-color: #3c3c3c;
border-bottom: 2px solid white; /* THEME 2 */
} --base00: #38428a;
--base-bg-color: #ffffff;
.navbar-nav { --base-font-color: rgb(53, 53, 53);
padding-bottom: 1em; --sec-font-color: #eaf2ff;
} --box-bg-color: #dddddd;
/* THEME 3 */
.masthead { --darkbase: #364B60;
margin-top: -21px; --lightbase: #2490E8;
color: white; --lighterbase: #60C2FF;
text-align: center; --whitebase: #FCFFFA;
min-height: 500px; --greybase: #B1B5C0;
} --fontbase: #34303a;
--fontsec: #5b5861;
.masthead .header { /* THEME 4 */
max-width: 700px; --darkbase: #263C4C;
margin: 0 auto; --lightbase: #598EB5;
font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif; --lighterbase: #5F98C2;
} --whitebase: #FCFFFA;
--greybase: #B1B5C0;
.masthead h1.header { --blackbase: #1A2A36;
margin-top: 1em; --fontbase: #34303a;
margin-bottom: 0em; --fontsec: #5b5861;
font-size: 4.5em; --primarybase: #4C7A9C;
line-height: 1.2em;
font-weight: normal;
}
.masthead h2 {
font-size: 1.7em;
font-weight: normal;
}
.masthead .btn {
margin: 1em 0;
}
/* Common styles for all types */ /* THEME INDEPENDENT COLORS */
.bs-callout { --errorbase: red;
padding: 20px; --warningbase: #fe7700;
margin: 20px 0; --validbase: #2dcc35;
border: 1px solid #eee; --infobase: var(--darkbase);
border-left-width: 5px;
border-radius: 3px;
/* FONTS */
--fontfamilybase: "Source Sans Pro", Helvetica, sans-serif;
/* DIMENSIONS */
--header-height: 80px;
--header-height-collapsed: 50px;
} }
.bs-callout p:last-child { * {
margin-bottom: 0; box-sizing: border-box;
padding: 0;
margin: 0;
} }
.bs-callout-info { body {
border-left-color: #1b809e; background-color: white;
color: var(--fontbase);
font-family: var(--fontfamilybase);
font-weight: 400;
font-size: 16px;
overflow-y: scroll;
} }
/* Space things out */ a,
.bs-docs-section { a:visited {
margin-bottom: 60px; color: var(--darkbase);
text-decoration: none;
font-weight: 600;
transition: color .2s ease, background-color .2s ease;
} }
.bs-docs-section:last-child { a:hover {
margin-bottom: 0; color: var(--lightbase);
} }
#message { ul {
margin-bottom: 40px; list-style-type: none;
}
.list--inline > li {
display: inline-block;
}
h1, h2, h3, h4, h5 {
font-weight: 600;
}
h1 {
font-size: 32px;
margin: 20px 0 10px;
}
h2 {
font-size: 24px;
margin: 10px 0 5px;
}
h3 {
font-size: 20px;
margin: 5px 0;
}
h4 {
font-size: 16px;
margin: 0;
}
table {
margin: 21px 0;
/*width: 100%;*/
}
th, td {
text-align: left;
padding: 0 13px 0 7px;
vertical-align: baseline;
}
th:first-child,
td:first-child {
padding-left: 0;
border-left: 0;
}
th {
border-left: 2px solid var(--greybase);
}
/* LAYOUT */
.main {
display: flex;
padding-right: 5vw;
min-height: calc(100vh - var(--header-height));
}
.main__content {
position: relative;
background-color: white;
padding: 0 40px;
padding-right: 0;
flex: 1;
z-index: 0;
> .container {
margin: 20px 0;
}
p {
margin: 10px 0;
}
}
.pseudo-focus {
outline: 5px auto var(--lightbase);
outline: 5px auto -webkit-focus-ring-color;
}
/* GENERAL BUTTON STYLES */
input[type="submit"],
input[type="button"],
button,
.btn {
outline: 0;
border: 0;
box-shadow: 0;
background-color: var(--lightbase);
color: white;
padding: 10px 17px;
min-width: 100px;
transition: all .1s;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
display: inline-block;
}
input.btn-primary,
button.btn-primary,
.btn.btn-primary {
background-color: var(--primarybase);
}
input.btn-info,
button.btn-info,
.btn.btn-info {
background-color: var(--infobase)
}
input[type="submit"][disabled],
input[type="button"][disabled],
button[disabled],
.btn[disabled] {
opacity: 0.3;
background-color: var(--greybase);
cursor: default;
}
input[type="submit"]:not([disabled]):hover,
input[type="button"]:not([disabled]):hover,
button:not([disabled]):hover,
.btn:not([disabled]):hover {
background-color: var(--lighterbase);
text-decoration: underline;
}
input[type="submit"].btn-info:hover,
input[type="button"].btn-info:hover,
button.btn-info:hover,
.btn.btn-info:hover {
background-color: var(--greybase)
} }

View File

@ -1,9 +0,0 @@
$newline never
#{fragment}
$case formLayout
$of FormStandard
$forall view <- views
<div :fvRequired view:.required :not $ fvRequired view:.optional :isJust $ fvErrors view:.has-error>
$if not (Blaze.null $ fvLabel view)
<label for=#{fvId view}>#{fvLabel view}
^{fvInput view}

View File

@ -10,4 +10,4 @@
<div .col-md-10 .col-lg-9> <div .col-md-10 .col-lg-9>
<div .bs-callout bs-callout-info well> <div .bs-callout bs-callout-info well>
<form .form-horizontal method=post action=@{actionUrl}#forms enctype=#{formEnctype}> <form .form-horizontal method=post action=@{actionUrl}#forms enctype=#{formEnctype}>
^{formWidget} ^{formWidget}

View File

@ -1,82 +1,69 @@
<div .masthead>
<div .container>
<div .row>
<h1 .header>
ReWorX - Demo
<h3>
Testumgebung für die Re-Implementierung von
<a href="https://uniworx.ifi.lmu.de/">
UniWorX
<div .container> <div .container>
<!-- Starting <h1>ReWorX - Demo
================================================== --> <h3>
<div .bs-docs-section> Testumgebung für die Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
<div .row> <p>
<div .col-lg-12> Die Reimplementierung von
<div .page-header> UniWorX ist noch nicht abgeschlossen.
<h2 #start>Übersicht
<p>
Die Reimplementierung von
UniWorX ist noch nicht abgeschlossen.
<div .alert .alert-danger>
Das System ist noch nicht produktiv
einsetzbar
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
<h2 #design>Design
<p>
Wir konzentrieren uns derzeit
ausschließlich auf die Funktionalität.
<p>
Insbesondere Formulare zeigen
alle Eingabefelder und Knöpfe
ohne eine gezielte Anordnung
und Reihenfolge.
Dies läßt sich leicht nachträglich einstellen.
<p>
Momentan werden noch keine speziellen Grafiken oder CSS verwendet;
sondern nur gewöhnliches Bootstrap3.
<div .bs-docs-section>
<div .row>
<div .col-lg-12>
<div .page-header>
<h3 #design>Teilweise funktionierende Abschnitte
<ul .list-group>
<li .list-group-item> <p .alert .alert-danger>Das System ist noch nicht produktiv einsetzbar
<a href=@{UsersR}>Benutzer Verwaltung
<li .list-group-item> <hr>
<a href=@{TermShowR}>Semester Verwaltung <div .container>
<a href=@{TermEditR}>Neues Semester anlegen <h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
<ul>
<li .list-group-item> <li .list-group-item>
<a href=@{CourseEditR}>Kurse anlegen <a href=@{UsersR}>Benutzer Verwaltung
editieren und anzeigen
<li .list-group-item> <li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen <a href=@{TermShowR}>Semester Verwaltung
<a href=@{TermEditR}>Neues Semester anlegen
<h3 #design>Funktionen zum Testen
<ul .list-group>
<li .list-group-item> <li .list-group-item>
Knopf-Test: <a href=@{CourseEditR}>Kurse anlegen, editieren und anzeigen
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
^{btnWdgt} <li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
<li .list-group-item>
<a href=@{CourseEditR}>Kurse anlegen <hr>
editieren und anzeigen <div .container>
<h2 .js-show-hide__toggle data-collapsed=true>Tabellen
<table .js-sortable>
<thead>
<tr>
<th .sorted-asc>ID
<th>TH1
<th>TH2
<th>TH3
<tbody>
<tr>
<td>0
<td>14
<td>CON2
<td>3
<tr>
<td>1
<td>5
<td>ONT2
<td>13
<tr>
<td>2
<td>CONT1
<td>NT2
<td>43
<tr>
<td>3
<td>43
<td>T2C2
<td>35
<hr>
<div .container>
<h2>Funktionen zum Testen
<ul>
<li>
Knopf-Test:
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
^{btnWdgt}

0
templates/home.julius Normal file
View File

View File

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

View File

@ -0,0 +1,177 @@
(function() {
'use strict';
window.utils = window.utils || {};
// makes <label> smaller if <input> is focussed
window.utils.reactiveInputLabel = function(input, label) {
// updates to dom
if (input.value.length > 0) {
label.classList.add('reactive-label--small');
}
// add event listeners
input.addEventListener('focus', function() {
label.classList.add('reactive-label--small');
});
label.addEventListener('click', function() {
label.classList.add('reactive-label--small');
input.focus();
});
input.addEventListener('blur', function() {
if (input.value.length < 1) {
label.classList.remove('reactive-label--small');
}
});
};
window.utils.reactiveFileUpload = function(input, parent) {
var currValidInputCount = 0;
var addMore = false;
var inputName = input.getAttribute('name');
// FileInput PseudoClass
function FileInput(container, input, label, remover) {
this.container = container;
this.input = input;
this.label = label;
this.remover = remover;
addListener(this);
this.addTo = function(parentElement) {
parentElement.appendChild(this.container);
}
this.remove = function() {
this.container.remove();
}
this.isValid = function() {
return this.container.classList.contains('file-input__container--valid');
}
}
function addNextInput() {
var inputs = parent.querySelectorAll('.file-input__container');
if (inputs[inputs.length - 1].classList.contains('file-input__container--valid')) {
makeInput(inputName).addTo(parent);
}
}
// updates submitbutton and form-group-stripe
function updateForm() {
var submitBtn = parent.parentElement.querySelector('[type=submit]');
parent.classList.remove('form-group--has-error');
if (currValidInputCount > 0) {
if (parent.classList.contains('form-group')) {
parent.classList.add('form-group--valid')
}
submitBtn.removeAttribute('disabled');
addNextInput();
} else {
if (parent.classList.contains('form-group')) {
parent.classList.remove('form-group--valid')
}
submitBtn.setAttribute('disabled', 'disabled');
}
}
// addseventlistener destInput
function addListener(fileInput) {
fileInput.input.addEventListener('change', function(event) {
if (fileInput.input.value.length > 0) {
// update label
var filePath = fileInput.input.value.replace(/\\/g, '/').split('/');
var fileName = filePath[filePath.length - 1];
fileInput.label.innerHTML = fileName;
// increase count if this field was empty previously
if (!fileInput.isValid()) {
currValidInputCount++;
}
fileInput.container.classList.add('file-input__container--valid')
// show next input
} else {
currValidInputCount--;
fileInput.remove();
}
updateForm();
});
fileInput.input.addEventListener('focus', function() {
fileInput.container.classList.add('pseudo-focus');
});
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()) {
currValidInputCount--;
}
fileInput.remove();
updateForm();
});
}
// create new wrapped input element with name name
function makeInput(name) {
var cont = document.createElement('div');
var desc = document.createElement('span');
var nextInput = document.createElement('input');
var remover = document.createElement('div');
cont.classList.add('file-input__container');
desc.classList.add('file-input__label', 'btn');
remover.classList.add('file-input__remover');
nextInput.setAttribute('name', name);
nextInput.setAttribute('type', 'file');
cont.appendChild(nextInput);
cont.appendChild(desc);
cont.appendChild(remover);
return new FileInput(cont, nextInput, desc, remover);
}
// initial setup
function setup() {
var newInput = makeInput(inputName);
input.remove();
newInput.addTo(parent);
updateForm();
}
setup();
}
window.utils.reactiveFormGroup = function(formGroup, input) {
// updates to dom
if (input.value.length > 0) {
formGroup.classList.add('form-group--valid');
} else {
formGroup.classList.remove('form-group--valid');
}
input.addEventListener('input', function() {
formGroup.classList.remove('form-group--has-error');
if (input.value.length > 0) {
formGroup.classList.add('form-group--valid');
} else {
formGroup.classList.remove('form-group--valid');
}
});
};
})();
document.addEventListener('DOMContentLoaded', function() {
// setup reactive labels
Array.from(document.querySelectorAll('.reactive-label')).forEach(function(label) {
var input = document.querySelector('#' + label.getAttribute('for'));
var parent = label.parentElement;
var type = input.getAttribute('type');
var isFileInput = /file/i.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');
if (isInFormGroup) {
window.utils.reactiveFormGroup(parent, input);
}
if (isFileInput) {
window.utils.reactiveFileUpload(input, parent);
}
if (isListening) {
window.utils.reactiveInputLabel(input, label);
} else {
label.classList.remove('reactive-label');
}
});
});

View File

@ -0,0 +1,306 @@
/* GENERAL STYLES FOR FORMS */
form {
margin: 20px 0;
}
/* TEXT INPUTS */
input[type="text"],
input[type="password"],
input[type="url"],
input[type="number"],
input[type="email"] {
background-color: rgba(0, 0, 0, 0.05);
padding: 7px 3px 7px;
outline: 0;
border: 0;
border-bottom: 2px solid var(--darkbase);
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
color: var(--fontbase);
transition: all .1s;
font-size: 16px;
min-width: 300px;
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="url"]:focus,
input[type="number"]:focus,
input[type="email"]:focus {
border-bottom-color: var(--lightbase);
background-color: transparent;
}
/* BUTTON STYLE SEE default-layout.lucius */
/* TEXTAREAS */
textarea {
outline: 0;
border: 0;
padding: 7px 4px;
min-width: 300px;
min-height: 100px;
font-family: var(--fontfamilybase);
font-size: 16px;
color: var(--fontbase);
background-color: rgba(0, 0, 0, 0.05);
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
border-bottom: 2px solid var(--darkbase);
}
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"] {
position: relative;
height: 20px;
width: 20px;
-webkit-appearance: none;
cursor: pointer;
}
input[type="checkbox"]::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
background-color: var(--lighterbase);
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
}
input[type="checkbox"]:checked::before {
background-color: var(--lightbase);
}
input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
/* CUSTOM CHECKBOXES AND RADIO BOXES */
/* Completely replaces legacy checkbox and radiobox */
.checkbox,
.radio {
position: relative;
margin: 3px;
> [type="checkbox"],
> [type="radio"] {
display: none;
}
> label {
display: block;
padding: 7px 13px 7px 30px;
background-color: var(--darkbase);
border-radius: 4px;
color: white;
cursor: pointer;
}
> label::before,
> label::after {
content: '';
position: absolute;
top: 15px;
left: 4px;
display: block;
width: 20px;
height: 20px;
background-color: white;
transition: all .2s;
}
> label::before {
width: 20px;
height: 2px;
transform: scale(0.8, 0.1);
}
> label::after {
width: 20px;
height: 2px;
transform: scale(0.8, 0.1);
}
> :checked + label {
background-color: var(--lightbase);
text-decoration: underline;
}
&:hover > label::before,
> :checked + label::before {
transform: scale(1, 1) rotate(45deg);
}
&:hover > label::after,
> :checked + label::after {
transform: scale(1, 1) rotate(-45deg);
}
}
.radio > label::before {
transform: scale(0.01, 0.01) rotate(45deg);
}
.radio > label::after {
transform: scale(0.01, 0.01) rotate(-45deg);
}
.radio::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
border-radius: 4px;
border: 2px solid white;
z-index: -1;
}
/* REACTIVE LABELS */
.reactive-label {
cursor: text;
color: var(--fontsec);
transform: translate(0, 0);
transition: all .1s;
}
.reactive-label--small {
cursor: default;
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);
}
.reactive-label--small {
transform: translate(2px, 0px);
color: var(--fontsec);
/*font-size: 14px;*/
}
}
/* CUSTOM FILE INPUT */
input[type="file"] {
color: white;
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
outline: 0;
border: 0;
}
.file-input__container {
grid-column-start: 2;
display: flex;
justify-content: space-between;
}
.file-input__label,
.file-input__remover {
display: block;
border-radius: 2px;
padding: 5px 13px;
color: var(--whitebase);
cursor: pointer;
}
.file-input__label {
text-align: left;
position: relative;
height: 30px;
}
.file-input__label.btn {
padding: 5px 13px;
}
.file-input__label::after,
.file-input__label::before {
position: absolute;
content: '';
background-color: white;
width: 16px;
height: 2px;
top: 14px;
top: 50%;
left: 12px;
left: 50%;
}
.file-input__label::after {
transform: translate(-50%, -50%) rotate(90deg);
}
.file-input__label::before {
transform: translate(-50%, -50%);
}
.file-input__remover {
display: none;
width: 40px;
height: 30px;
text-align: center;
background-color: var(--warningbase);
position: relative;
margin-left: 10px;
}
.file-input__remover::before {
position: absolute;
content: '';
width: 16px;
height: 2px;
top: 14px;
left: 12px;
background-color: white;
}
.file-input__container--valid > .file-input__label {
background-color: var(--lightbase);
}
.file-input__container--valid > .file-input__label::before,
.file-input__container--valid > .file-input__label::after {
content: none;
}
.file-input__container--valid > .file-input__remover {
display: block;
}
@media (max-width: 999px) {
.file-input__container {
grid-column-start: 1;
}
}

View File

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

View File

@ -0,0 +1,59 @@
/**
* div.js-show-hide
* div.js-show-hide-toggle
* toggle here
* div
* content here
*/
document.addEventListener('DOMContentLoaded', function() {
var elements = Array.from(document.querySelectorAll('.js-show-hide__toggle')),
toggles = [],
path = new URL(window.location.href).pathname;
function addEventHandler(el) {
el.addEventListener('click', function elClickListener() {
var toggle = toggles[el.dataset.index];
toggle.collapsed = !toggle.collapsed;
toggle.parent.classList.toggle('js-show-hide--collapsed', toggle.collapsed);
updateLocalStorage();
});
}
function updateLocalStorage(id) {
var lsData = getStateFromLocalStorage();
lsData[path] = toggles.map(t => {
return {id: t.index, collapsed: t.collapsed};
});
window.localStorage.setItem('showHidesToggles', JSON.stringify(lsData));
}
function collapsedStateInLocalStorage(id, fallBack) {
var lsData = getStateFromLocalStorage();
if (lsData[path] && lsData[path][id] && lsData[path][id].id === id) {
return lsData[path][id].collapsed;
}
return fallBack;
}
function getStateFromLocalStorage() {
return JSON.parse(window.localStorage.getItem('showHidesToggles')) || {};
}
elements.forEach(function(el, i) {
el.dataset.index = i;
var coll = collapsedStateInLocalStorage(i, el.dataset.collapsed === 'true');
if (coll) {
el.parentElement.classList.add('js-show-hide--collapsed')
}
Array.from(el.parentElement.children).forEach(function(el) {
if (!el.classList.contains('js-show-hide__toggle')) {
el.classList.add('js-show-hide__target');
}
});
toggles.push({index: i, collapsed: coll, parent: el.parentElement});
addEventHandler(el);
});
});

View File

@ -0,0 +1,44 @@
.js-show-hide {
position: relative;
}
.js-show-hide__toggle {
position: relative;
}
.js-show-hide__toggle:hover {
cursor: pointer;
}
.js-show-hide__toggle::before {
content: '';
position: absolute;
width: 0;
height: 0;
border-right: 8px solid transparent;
border-bottom: 8px solid transparent;
}
.js-show-hide__toggle::before,
.js-show-hide--collapsed .js-show-hide__toggle:hover::before {
left: -28px;
top: 10px;
border-left: 8px solid transparent;
border-top: 8px solid var(--lightbase);
}
.js-show-hide__toggle:hover::before,
.js-show-hide--collapsed .js-show-hide__toggle::before {
border-left: 8px solid var(--lightbase);
border-top: 8px solid transparent;
top: 5px;
left: -22px;
}
.js-show-hide__target {
}
.js-show-hide--collapsed > .js-show-hide__target {
display: none;
}

View File

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

View File

@ -0,0 +1,88 @@
/**
* delcare a table as sortable by adding class 'js-sortable'
*/
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);
});
});

View File

@ -0,0 +1,31 @@
table.js-sortable th {
cursor: pointer;
position: relative;
padding-right: 20px;
}
table.js-sortable th.sorted-asc,
table.js-sortable th.sorted-desc {
color: var(--darkbase);
}
table.js-sortable th.sorted-asc::after,
table.js-sortable th.sorted-desc::after {
content: '';
position: absolute;
right: 0;
top: 15px;
width: 0;
height: 0;
transform: translateY(-100%);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
}
table.js-sortable th.sorted-asc::after {
border-top: 8px solid var(--lightbase);
}
table.js-sortable th.sorted-desc::after {
border-bottom: 8px solid var(--lightbase);
}

View File

@ -1,6 +1,8 @@
<form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}> <div .container>
^{uploadWidget} <form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}>
^{uploadWidget}
<form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}> <div .container>
^{subTable} <form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}>
<button .btn .btn-default type=submit >Markierte herunterladen ^{subTable}
<button .btn .btn-default type=submit >Markierte herunterladen

View File

@ -0,0 +1,20 @@
<aside .main__aside>
<div .asidenav>
<div .asidenav__box--dont-hide>
<ul .asidenav__list>
$forall menuType <- menuTypes
$case menuType
$of NavbarLeft (MenuItem label mIcon route _)
<li .asidenav__list-item :Just route == mcurrentRoute:.asidenav__list-item--active>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<a .asidenav__link href=@{route}>#{label}
$of _
<div .asidenav__box>
<h3 .asidenav__box-title>WiSe 17/18
<ul .asidenav__list.asidenav__list--padded>
<li>Vorlesung 1
<li>Vorlesung 2
<li>Vorlesung 3
<li>Vorlesung 4

View File

@ -0,0 +1,55 @@
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.aside = function(el) {
var asideEl = el;
var collapsed = false;
var collClass = 'main__aside--collapsed';
var animClass = 'main__aside--transitioning';
init();
function init() {
var collLS = window.localStorage.getItem('asidenavCollapsed') === 'true';
if (document.body.getBoundingClientRect().width < 999 || collLS) {
asideEl.classList.add(collClass);
collapsed = true;
}
}
function check() {
if (collapsed && !hasClass() || !collapsed && hasClass()) {
asideEl.classList.add(animClass);
asideEl.classList.toggle(collClass, collapsed);
window.localStorage.setItem('asidenavCollapsed', collapsed);
}
}
function hasClass() {
return asideEl.classList.contains(collClass);
}
asideEl.addEventListener('click', function(event) {
if (event.target === asideEl) {
collapsed = !collapsed;
check();
}
});
asideEl.addEventListener('transitionend', function(event) {
if (event.propertyName === 'opacity') {
asideEl.classList.remove(animClass);
}
});
window.addEventListener('resize', function() {
collapsed = document.body.getBoundingClientRect().width < 999;
check();
});
};
})();
document.addEventListener('DOMContentLoaded', function() {
utils.aside(document.querySelector('.main__aside'));
});

View File

@ -0,0 +1,94 @@
.main__aside {
flex-shrink: 0;
background-color: var(--darkbase);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1;
width: 300px;
overflow: hidden;
}
.main__aside--transitioning {
transition: width .2s ease;
}
.main__aside--transitioning .asidenav__box{
transition: opacity .2s ease;
}
.main__aside--collapsed:hover {
overflow: visible;
}
.main__aside--collapsed {
width: 50px;
}
.main__aside--collapsed .asidenav__box {
opacity: 0;
}
.main__aside--collapsed .asidenav__box--dont-hide {
opacity: 1;
}
.main__aside--collapsed:not(.main__aside--transitioning) .asidenav__box {
height: 0;
padding: 0;
margin: 0;
visibility: hidden;
}
.asidenav {
width: 300px;
margin-top: 20px;
color: white;
}
.asidenav__box {
margin: 10px 0;
padding: 10px 0;
border-bottom: 4px solid var(--whitebase);
background-color: var(--darkbase);
}
.asidenav__box-title {
padding: 7px 13px;
}
.asidenav__list--padded {
padding: 0 13px;
}
.asidenav__list-item {
position: relative;
background-color: white;
color: var(--darkbase);
.glyphicon {
position: absolute;
z-index: 1;
top: 5px;
}
&:not(.asidenav__list-item--active):hover {
color: white;
background-color: var(--darkbase);
.asidenav__link {
color: white;
}
}
}
.asidenav__list-item--active {
background-color: var(--darkbase);
color: white;
.asidenav__link {
pointer-events: none;
color: white;
}
}
.asidenav__link {
position: relative;
display: block;
line-height: 50px;
margin: 4px 0;
padding-left: 54px;
color: var(--darkbase);
z-index: 1;
}

View File

@ -0,0 +1,7 @@
<div .breadcrumbs__container>
<ul .breadcrumbs__list.list--inline>
$forall bc <- parents
<li .breadcrumbs__item>
<a .breadcrumbs__link href="@{fst bc}">#{snd bc}
&gt;
<li .breadcrumbs__item--active>#{title}

View File

@ -0,0 +1,17 @@
.breadcrumbs__container {
position: absolute;
color: white;
left: 340px;
top: 7px;
z-index: 100;
transition: left .2s ease;
}
.breadcrumbs__container .breadcrumbs__link {
color: white;
}
@media (max-width: 999px) {
.breadcrumbs__container {
left: 90px;
}
}

View File

@ -0,0 +1,9 @@
$newline never
#{fragment}
$case formLayout
$of FormStandard
$forall view <- views
<div .form-group :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error>
$if not (Blaze.null $ fvLabel view)
<label .form-group__label .reactive-label for=#{fvId view}>#{fvLabel view}
^{fvInput view}

View File

@ -0,0 +1,51 @@
(function() {
'use strict';
window.utils = window.utils || {};
// registers input-listener for each element in <elements> (array) and
// enables <button> if <fn> for these elements returns true
window.utils.reactiveButton = function(elements, button, fn) {
if (elements.length == 0) {
return false;
}
var checkboxes = elements[0].getAttribute('type') === 'checkbox';
var eventType = checkboxes ? 'change' : 'input';
updateButtonState();
elements.forEach(function(el) {
el.addEventListener(eventType, function() {
updateButtonState();
});
});
function updateButtonState() {
if (fn.call(null, elements)) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', 'true');
}
}
};
})();
document.addEventListener('DOMContentLoaded', function() {
// auto reactiveButton submit-buttons with required fields
var forms = document.querySelectorAll('form');
Array.from(forms).forEach(function(form) {
var requireds = form.querySelectorAll('[required]');
var submitBtn = form.querySelector('[type=submit]');
if (submitBtn && requireds) {
window.utils.reactiveButton(Array.from(requireds), submitBtn, function(inputs) {
var done = true;
inputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
});
}
});
});

View File

View File

@ -0,0 +1,10 @@
<div>
<h2>A little lorem ipsum to make the page scrollable
Aute velit consectetur consequat excepteur ut in qui do reprehenderit sit consequat occaecat incididunt sint eu. Nulla cillum quis et sit labore aliquip et ad excepteur duis elit deserunt aute. Anim esse reprehenderit et ipsum et sit quis minim enim sint et pariatur duis nostrud. Do minim dolore adipisicing ullamco ad aute veniam id magna est proident tempor labore non. Mollit et laboris duis esse commodo ex tempor.
Elit labore qui dolor ea non ut anim occaecat do aliqua cillum qui esse. Aliqua nisi non veniam labore esse irure velit qui veniam labore consectetur ut. Tempor laboris anim officia veniam nostrud cupidatat reprehenderit aute incididunt nostrud dolore occaecat deserunt pariatur in non dolore.
Proident anim pariatur eu laborum Lorem duis cillum est anim magna sit pariatur eu. Nulla consectetur quis ea sunt proident sint laborum. Esse est ea Lorem mollit aute excepteur fugiat ipsum ipsum ad irure do reprehenderit voluptate. Sint tempor nulla mollit voluptate pariatur aliquip culpa amet ullamco fugiat incididunt minim sint ipsum exercitation eiusmod ad. Nostrud ipsum nulla cillum ex aute elit voluptate proident proident magna ut. Ipsum officia cillum officia nostrud enim fugiat ut. Voluptate laboris aute dolore incididunt aliquip sit sit. Ea ut laboris Lorem ea ad non nostrud aute tempor non nisi.
Qui cupidatat nisi id et excepteur sint aliquip fugiat sint reprehenderit aliquip enim anim aliqua sint dolore proident. Voluptate labore tempor laboris nisi eiusmod sunt occaecat deserunt adipisicing. Consectetur fugiat quis non ad laboris aliqua voluptate in eu id. Duis velit cillum aliquip dolor non ea mollit incididunt elit ex excepteur aute consequat amet.
Aute velit consectetur consequat excepteur ut in qui do reprehenderit sit consequat occaecat incididunt sint eu. Nulla cillum quis et sit labore aliquip et ad excepteur duis elit deserunt aute. Anim esse reprehenderit et ipsum et sit quis minim enim sint et pariatur duis nostrud. Do minim dolore adipisicing ullamco ad aute veniam id magna est proident tempor labore non. Mollit et laboris duis esse commodo ex tempor.
Elit labore qui dolor ea non ut anim occaecat do aliqua cillum qui esse. Aliqua nisi non veniam labore esse irure velit qui veniam labore consectetur ut. Tempor laboris anim officia veniam nostrud cupidatat reprehenderit aute incididunt nostrud dolore occaecat deserunt pariatur in non dolore.
Proident anim pariatur eu laborum Lorem duis cillum est anim magna sit pariatur eu. Nulla consectetur quis ea sunt proident sint laborum. Esse est ea Lorem mollit aute excepteur fugiat ipsum ipsum ad irure do reprehenderit voluptate. Sint tempor nulla mollit voluptate pariatur aliquip culpa amet ullamco fugiat incididunt minim sint ipsum exercitation eiusmod ad. Nostrud ipsum nulla cillum ex aute elit voluptate proident proident magna ut. Ipsum officia cillum officia nostrud enim fugiat ut. Voluptate laboris aute dolore incididunt aliquip sit sit. Ea ut laboris Lorem ea ad non nostrud aute tempor non nisi.
Qui cupidatat nisi id et excepteur sint aliquip fugiat sint reprehenderit aliquip enim anim aliqua sint dolore proident. Voluptate labore tempor laboris nisi eiusmod sunt occaecat deserunt adipisicing. Consectetur fugiat quis non ad laboris aliqua voluptate in eu id. Duis velit cillum aliquip dolor non ea mollit incididunt elit ex excepteur aute consequat amet.

View File

@ -0,0 +1,23 @@
<div .navbar-container>
<nav .navbar.js-sticky-navbar>
<ul .navbar__list.list--inline>
$forall menuType <- menuTypes
$case menuType
$of NavbarRight (MenuItem label mIcon route _)
<li .navbar__list-item :Just route == mcurrentRoute:.navbar__list-item--active>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<a .navbar__link href=@{route}>#{label}
$of NavbarSecondary (MenuItem label mIcon route _)
<li .navbar__list-item.navbar__list-item--secondary :Just route == mcurrentRoute:.navbar__list-item--active>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<a .navbar__link href=@{route}>#{label}
$of _
<!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute
^{breadcrumbs}
<div .navbar__pushdown>

View File

@ -0,0 +1,28 @@
/**
* .js-sticky-navbar
* ul
* li Item 1
* li Item 2
*/
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();
});

View File

@ -0,0 +1,111 @@
.navbar {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
width: 100%;
height: var(--header-height);
line-height: var(--header-height);
padding-right: 5vw;
background: var(--darkbase); /* Old browsers */
background: -moz-linear-gradient(bottom, var(--darkbase) 0%, #425d79 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(bottom, var(--darkbase) 0%,#425d79 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(to top, var(--darkbase) 0%,#425d79 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
color: white;
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
z-index: 10;
overflow: hidden;
}
.navbar .navbar__link {
position: relative;
display: inline-block;
height: 100%;
padding: 0 13px;
color: white;
text-transform: uppercase;
}
.navbar .navbar__link.glyphicon::before {
left: 50%;
top: -20px;
transform: translateX(-50%);
margin: 0;
}
.navbar__list-item {
position: relative;
padding-top: 13px;
> .glyphicon {
position: absolute;
width: 100%;
}
> .glyphicon::before {
height: 40px;
margin: 0;
left: 50%;
transform: translateX(-50%);
}
}
.navbar__list-item--secondary {
margin-left: 20px;
color: var(--greybase);
}
.navbar__list-item--secondary + .navbar__list-item--secondary {
margin-left: 0;
border-left: 0;
}
.navbar__list-item--active {
background-color: white;
color: var(--darkbase);
}
.navbar__list-item--active > .navbar__link {
color: var(--darkbase);
pointer-events: none;
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover {
background-color: var(--darkbase);
color: var(--whitebase);
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover > .navbar__link {
color: var(--whitebase);
}
.navbar__list-item--secondary > .navbar__link {
color: var(--greybase);
}
.navbar__link {
transition: opacity .2s ease;
}
.navbar--sticky {
position: fixed;
top: 0;
left: 0;
height: var(--header-height-collapsed);
line-height: var(--header-height-collapsed);
z-index: 100;
transition: height 0.2s ease, line-height 0.2s ease;
.navbar__link {
opacity: 0;
}
.breadcrumbs__container {
top: 0px;
}
}
.navbar__pushdown {
display: none;
background-color: var(--darkbase);
height: var(--header-height);
}
.navbar--sticky + .navbar__pushdown {
display: block;
}