merged from master

This commit is contained in:
SJost 2019-02-17 15:08:56 +01:00
commit 76ac563627
52 changed files with 716 additions and 329 deletions

View File

@ -1,5 +1,5 @@
* Version 30.01.2019
Designänderungen
* Version 16.01.2019
@ -7,7 +7,7 @@
Links für Bequemlichkeiten hinzugefügt (z.B. aktuelles Übungsblatt)
Liste zugewiesener Abgaben lassen sich nun filtern
Bugfix: Wenn zwischen Anzeige und Empfang eines Tabellen-Formulars Zeilen verschwinden wird nun eine sinnvolle Fehlermeldung angezeigt
* Version 30.11.2018
@ -65,11 +65,6 @@
Bugfixes, wählbares Format für Datum
* Version 04.07.2018
Hinweis eingefügt, dass alle Daten des Systems spätestens im Dezember 2018
gelöscht werden.
* Version 03.07.2018
Willkommen bei Uni2work aka "You-need-to-work!"

BIN
assets/favicon-0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
assets/favicon-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

4
assets/logo-o2.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="158" height="158" viewBox="0 0 158 158" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M31 16.2494C12.1534 30.6875 0 53.4245 0 79C0 122.63 35.3695 158 79 158C122.63 158 158 122.63 158 79C158 53.4245 145.847 30.6875 127 16.2494V39.7542C135.75 50.4433 141 64.1086 141 79C141 113.242 113.242 141 79 141C44.7583 141 17 113.242 17 79C17 64.1086 22.25 50.4433 31 39.7542V16.2494Z" fill="#0A9342"/>
<path d="M119.111 121H40.5371V107.597L79.4631 65.1392C85.0813 58.879 89.0675 53.6621 91.4218 49.4886C93.8296 45.2616 95.0335 41.0345 95.0335 36.8075C95.0335 31.2429 93.4551 26.7483 90.2982 23.3239C87.1948 19.8995 82.9945 18.1873 77.6974 18.1873C71.3836 18.1873 66.4878 20.1135 63.0099 23.966C59.5319 27.8184 57.793 33.0888 57.793 39.7771H38.2899C38.2899 32.6608 39.8951 26.2668 43.1055 20.5951C46.3693 14.8699 50.9977 10.4288 56.9904 7.27195C63.0366 4.11506 69.9925 2.53662 77.8579 2.53662C89.2013 2.53662 98.1369 5.39922 104.665 11.1244C111.246 16.7961 114.537 24.6616 114.537 34.7208C114.537 40.553 112.878 46.6795 109.561 53.1003C106.297 59.4675 100.919 66.7177 93.4283 74.8506L64.8558 105.43H119.111V121Z" fill="#0A9342"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -27,6 +27,7 @@ notification-rate-limit: 3600
notification-collate-delay: 300
notification-expiration: 259201
session-timeout: 7200
maximum-content-length: 52428800
log-settings:
detailed: "_env:DETAILED_LOGGING:false"

View File

@ -47,7 +47,7 @@ LectureStart: Beginn Vorlesungen
Course: Kurs
CourseShort: Kürzel
CourseCapacity: Kapazität
CourseCapacityTip: Falls angegeben wird die Anzahl an Kursanmeldungen, die zugelassen werden, beschränkt
CourseCapacityTip: Anzahl erlaubter Kursanmeldungen, leer lassen für unbeschränkte Kurskapazität
CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei.
CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet.
CourseRegisterOk: Sie wurden angemeldet
@ -75,14 +75,15 @@ CourseDescription: Beschreibung
CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet
CourseHomepage: Homepage
CourseShorthand: Kürzel
CourseShorthandUnique: Muss innerhalb des Semesters eindeutig sein
CourseShorthandUnique: Muss innerhalb Institut und Semester eindeutig sein
CourseSemester: Semester
CourseSchool: Institut
CourseSchoolShort: Fach
CourseSecretTip: Anmeldung zum Kurs erfordert Eingabe des Passworts, sofern gesetzt
CourseSecretFormat: beliebige Zeichenkette
CourseRegisterFromTip: Ohne Datum ist KEINE eigenständige Anmeldung von Studierenden möglich
CourseRegisterToTip: Anmeldung darf auch ohne Begrenzung möglich sein
CourseDeregisterUntilTip: Abmeldung darf auch ohne Begrenzung möglich sein
CourseRegisterToTip: Anmeldung darf auch unbegrenzt offen bleiben
CourseDeregisterUntilTip: Abmeldung darf auch unbegrenzt erlaubt bleiben
CourseFilterSearch: Volltext-Suche
CourseFilterRegistered: Registriert
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
@ -231,6 +232,7 @@ HomeHeading: Aktuelle Termine
LoginHeading: Authentifizierung
LoginTitle: Authentifizierung
ProfileHeading: Benutzereinstellungen
ProfileFor: Benutzereinstellungen für
ProfileDataHeading: Gespeicherte Benutzerdaten
ImpressumHeading: Impressum
SystemMessageHeading: Uni2work Statusmeldung
@ -333,11 +335,15 @@ MultiSinkException name@Text error@Text: In Abgabe #{name} ist ein Fehler aufget
NoTableContent: Kein Tabelleninhalt
NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter
AdminUserHeading: Benutzeradministration
AccessRightsFor: Berechtigungen für
AdminFor: Administrator
LecturerFor: Dozent
LecturersFor: Dozenten
UserListTitle: Komprehensive Benutzerliste
AccessRightsSaved: Berechtigungsänderungen wurden gespeichert.
Date: Datum
DateTimeFormat: Datums- und Uhrzeitformat
DateFormat: Datumsformat
TimeFormat: Uhrzeitformat
@ -489,6 +495,7 @@ CorrGrade: Korrekturen eintragen
UserAccountDeleted name@Text: Konto für #{name} wurde gelöscht!
HelpTitle : Hilfe
HelpAnswer: Antworten an
HelpUser: Meinen Benutzeraccount
HelpAnonymous: Keine Antwort (Anonym)
@ -498,6 +505,9 @@ HelpProblemPage: Problematische Seite
HelpIntroduction: Wenn Ihnen die Benutzung dieser Webseite Schwierigkeiten bereitet oder Sie einen verbesserbaren Umstand entdecken bitten wir Sie uns das zu melden, auch wenn Sie Ihr Problem bereits selbst lösen konnten. Wir passen die Seite ständig an und versuchen sie auch für zukünftige Benutzer so einsichtig wie möglich zu halten.
HelpSent: Ihre Supportanfrage wurde weitergeleitet.
InfoLecturerTitle: Hinweise für Veranstalter
SystemMessageFrom: Sichtbar ab
SystemMessageTo: Sichtbar bis
SystemMessageAuthenticatedOnly: Nur angemeldet
@ -560,7 +570,8 @@ ErrorCryptoIdMismatch: Verschlüsselte Id der Abgabe passte nicht zu anderen Dat
InvalidRoute: Konnte URL nicht interpretieren
MenuHome: Aktuell
MenuVersion: Impressum
MenuImpressum: Impressum
MenuVersion: Versionsgeschichte
MenuHelp: Hilfe
MenuProfile: Anpassen
MenuLogin: Login
@ -598,6 +609,7 @@ MenuCorrectionsCreate: Abgaben registrieren
MenuCorrectionsGrade: Abgaben bewerten
MenuAuthPreds: Authorisierungseinstellungen
AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate werden nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären. Diese Einstellungen gelten nur temporär bis Ihre Sitzung abgelaufen ist (d.h. bis ihr Browser-Cookie abgelaufen ist).
AuthPredsActive: Aktive Authorisierungsprädikate
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
AuthTagFree: Seite ist universell zugänglich

View File

@ -114,6 +114,7 @@ dependencies:
- memcached-binary
- directory-tree
- lifted-base
- hsass
other-extensions:
- GeneralizedNewtypeDeriving

18
routes
View File

@ -11,7 +11,7 @@
--
-- Access Tags:
-- !free -- free for all
-- !lecturer -- lecturer for this course (or the school, if route is not connected to a course)
-- !lecturer -- lecturer for this course (or for any school, if route is not connected to a course)
-- !corrector -- corrector for this sheet (or the submission, if route is connected to a submission, or the course, if route is not connected to a sheet, or any course, if route is not connected to a course)
-- !registered -- participant for this course (no effect outside of courses)
-- !participant -- connected with a given course (not necessarily registered), i.e. has a submission, is a corrector, etc. (no effect outside of courses)
@ -35,24 +35,28 @@
/ HomeR GET !free
/users UsersR GET -- no tags, i.e. admins only
/users/#CryptoUUIDUser AdminUserR GET !development
/users/#CryptoUUIDUser AdminUserR GET POST !development
/users/#CryptoUUIDUser/hijack AdminHijackUserR POST !adminANDno-escalation
/admin/test AdminTestR GET POST
/admin/errMsg AdminErrMsgR GET POST
/info VersionR GET !free
/impressum ImpressumR GET !free
/version VersionR GET !free
/info InfoR GET !free
/help HelpR GET POST !free
/help/lecturer InfoLecturerR GET !lecturer
/profile ProfileR GET POST !free
/profile/data ProfileDataR GET POST !free
/authpreds AuthPredsR GET POST !free
/user ProfileR GET POST !free
/user/profile ProfileDataR GET POST !free
/user/authpreds AuthPredsR GET POST !free
/term TermShowR GET !free
/term/current TermCurrentR GET !free
/term/edit TermEditR GET POST
/term/#TermId/edit TermEditExistR GET
!/term/#TermId TermCourseListR GET !free
!/term/#TermId/#SchoolId TermSchoolCourseListR GET !free
!/term/#TermId/#SchoolId TermSchoolCourseListR GET !free
/school SchoolListR GET !development
/school/#SchoolId SchoolShowR GET !development

View File

@ -430,6 +430,7 @@ tagAccessPredicate AuthLecturer = APDB $ \route _ -> case route of
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c>0) (unauthorizedI MsgUnauthorizedLecturer)
return Authorized
-- lecturer for any school will do
_ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
void . maybeMExceptT (unauthorizedI MsgUnauthorizedSchoolLecturer) $ selectFirst [UserLecturerUser ==. authId] []
@ -752,7 +753,7 @@ instance Yesod UniWorX where
(getCachedDate, _) <- clientSessionDateCacher appSessionTimeout
return . Just $ clientSessionBackend appSessionKey getCachedDate
maximumContentLength _ _ = Just $ 50 * 2^20
maximumContentLength UniWorX{appSettings=AppSettings{appMaximumContentLength}} _ = appMaximumContentLength
-- Yesod Middleware allows you to run code before and after each handler function.
-- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks.
@ -835,7 +836,7 @@ instance Yesod UniWorX where
NotAuthenticated -> [whamlet|<p>_{MsgErrorResponseNotAuthenticated}|]
PermissionDenied err' -> [whamlet|<p .errMsg>#{err'}|]
BadMethod method -> [whamlet|<p>_{MsgErrorResponseBadMethod (decodeUtf8 method)}|]
fmap toTypedContent . siteLayout (Just . toHtml . mr $ ErrorResponseTitle err) $ do
fmap toTypedContent . siteLayout (toWgt . mr $ ErrorResponseTitle err) $ do
toWidget
[cassius|
.errMsg
@ -844,7 +845,7 @@ instance Yesod UniWorX where
|]
errPage
defaultLayout = siteLayout Nothing
defaultLayout = siteLayout' Nothing
-- The page to be redirected to when authentication is required.
authRoute _ = Just $ AuthR LoginR
@ -887,9 +888,21 @@ instance Yesod UniWorX where
makeLogger = readTVarIO . snd . appLogger
siteLayout :: Maybe Html -- ^ Optionally override `pageHeading`
siteLayoutMsg :: (RenderMessage site msg, site ~ UniWorX) => msg -> Widget -> Handler Html
siteLayoutMsg msg widget = do
mr <- getMessageRender
siteLayout (toWgt $ mr msg) widget
siteLayoutMsg' :: (RenderMessage site msg, site ~ UniWorX) => msg -> Widget -> Handler Html
siteLayoutMsg' = siteLayout . i18nHeading
siteLayout :: Widget -- ^ `pageHeading`
-> Widget -> Handler Html
siteLayout headingOverride widget = do
siteLayout = siteLayout' . Just
siteLayout' :: Maybe Widget -- ^ Optionally override `pageHeading`
-> Widget -> Handler Html
siteLayout' headingOverride widget = do
master <- getYesod
let AppSettings { appUserDefaults = UserDefaultConf{..}, .. } = appSettings master
@ -961,8 +974,10 @@ siteLayout headingOverride widget = do
asidenav = $(widgetFile "widgets/asidenav")
footer :: Widget
footer = $(widgetFile "widgets/footer")
alerts :: Widget
alerts = $(widgetFile "widgets/alerts/alerts")
contentHeadline :: Maybe Widget
contentHeadline = (toWidget <$> headingOverride) <|> (pageHeading =<< mcurrentRoute)
contentHeadline = headingOverride <|> (pageHeading =<< mcurrentRoute)
breadcrumbsWgt :: Widget
breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs")
pageaction :: Widget
@ -974,23 +989,24 @@ siteLayout headingOverride widget = do
hasPrimaryPageActions = any (is _PageActionPrime) $ toListOf (traverse . _1 . _menuItemType) menuTypes
pc <- widgetToPageContent $ do
addScript $ StaticR js_zepto_js
addScript $ StaticR js_fetchPolyfill_js
addScript $ StaticR js_urlPolyfill_js
addScript $ StaticR js_featureChecker_js
addScript $ StaticR js_flatpickr_js
addScript $ StaticR js_tabber_js
addStylesheet $ StaticR css_flatpickr_css
addStylesheet $ StaticR css_tabber_css
addScript $ StaticR js_vendor_zepto_js
addScript $ StaticR js_vendor_flatpickr_js
addScript $ StaticR js_polyfills_fetchPolyfill_js
addScript $ StaticR js_polyfills_urlPolyfill_js
addScript $ StaticR js_utils_featureChecker_js
addScript $ StaticR js_utils_tabber_js
addScript $ StaticR js_utils_alerts_js
addStylesheet $ StaticR css_vendor_flatpickr_css
addStylesheet $ StaticR css_vendor_fontawesome_css
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_fontawesome_css
addStylesheet $ StaticR css_utils_tabber_css
addStylesheet $ StaticR css_utils_alerts_scss
$(widgetFile "default-layout")
$(widgetFile "standalone/modal")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/inputs")
$(widgetFile "standalone/tooltip")
$(widgetFile "standalone/tabber")
$(widgetFile "standalone/alerts")
$(widgetFile "standalone/datepicker")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
@ -1022,25 +1038,28 @@ applySystemMessages = liftHandlerT . runDB . runConduit $ selectSource [] [] .|
-- Define breadcrumbs.
instance YesodBreadcrumbs UniWorX where
breadcrumb (AuthR _) = return ("Login" , Just HomeR)
breadcrumb HomeR = return ("Uni2work", Nothing)
breadcrumb UsersR = return ("Benutzer", Just HomeR)
breadcrumb AdminTestR = return ("Test" , Just HomeR)
breadcrumb (AdminUserR _) = return ("Users" , Just UsersR)
breadcrumb VersionR = return ("Impressum" , Just HomeR)
breadcrumb (AuthR _) = return ("Login" , Just HomeR)
breadcrumb HomeR = return ("Uni2work" , Nothing)
breadcrumb UsersR = return ("Benutzer" , Just HomeR)
breadcrumb AdminTestR = return ("Test" , Just HomeR)
breadcrumb (AdminUserR _) = return ("Users" , Just UsersR)
breadcrumb VersionR = return ("Impressum" , Just HomeR)
breadcrumb HelpR = return ("Hilfe" , Just HomeR)
breadcrumb InfoLecturerR = return ("Veranstalter" , Just HelpR)
breadcrumb ProfileR = return ("Profile" , Just HomeR)
breadcrumb ProfileDataR = return ("Data" , Just ProfileR)
breadcrumb ProfileR = return ("User" , Just HomeR)
breadcrumb ProfileDataR = return ("Profile" , Just ProfileR)
breadcrumb AuthPredsR = return ("Authentifizierung", Just ProfileR)
breadcrumb TermShowR = return ("Semester" , Just HomeR)
breadcrumb TermCurrentR = return ("Aktuell" , Just TermShowR)
breadcrumb TermEditR = return ("Neu" , Just TermCurrentR)
breadcrumb (TermEditExistR tid) = return ("Editieren" , Just $ TermCourseListR tid)
breadcrumb (TermCourseListR (unTermKey -> tid)) = getMessageRender <&> \mr -> (mr $ ShortTermIdentifier tid, Nothing)
breadcrumb (TermCourseListR (unTermKey -> tid)) = getMessageRender <&> \mr -> (mr $ ShortTermIdentifier tid, Just CourseListR)
breadcrumb (TermSchoolCourseListR tid ssh) = return (CI.original $ unSchoolKey ssh, Just $ TermCourseListR tid)
breadcrumb CourseListR = return ("Kurse" , Just HomeR)
breadcrumb CourseListR = return ("Kurse" , Nothing)
breadcrumb CourseNewR = return ("Neu" , Just CourseListR)
breadcrumb (CourseR tid ssh csh CShowR) = return (CI.original csh, Just $ TermSchoolCourseListR tid ssh)
-- (CourseR tid ssh csh CRegisterR) -- is POST only
@ -1090,18 +1109,18 @@ submissionList tid csh shn uid = E.select . E.from $ \(course `E.InnerJoin` shee
defaultLinks :: (MonadHandler m, HandlerSite m ~ UniWorX) => m [MenuItem]
defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header.
[ return MenuItem
{ menuItemType = NavbarAside
{ menuItemType = NavbarAside
, menuItemLabel = MsgMenuHome
, menuItemIcon = Just "home"
, menuItemIcon = Just "home"
, menuItemRoute = SomeRoute HomeR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
, return MenuItem
{ menuItemType = Footer
, menuItemLabel = MsgMenuVersion
{ menuItemType = Footer
, menuItemLabel = MsgMenuImpressum
, menuItemIcon = Just "book"
, menuItemRoute = SomeRoute VersionR
, menuItemRoute = SomeRoute ImpressumR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
@ -1109,7 +1128,7 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
mCurrentRoute <- getCurrentRoute
return MenuItem
{ menuItemType = NavbarRight
{ menuItemType = NavbarRight
, menuItemLabel = MsgMenuHelp
, menuItemIcon = Just "question"
, menuItemRoute = SomeRoute (HelpR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mCurrentRoute])
@ -1117,57 +1136,57 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
, menuItemAccessCallback' = return True
}
, return MenuItem
{ menuItemType = NavbarRight
{ menuItemType = NavbarRight
, menuItemLabel = MsgMenuProfile
, menuItemIcon = Just "cogs"
, menuItemIcon = Just "cogs"
, menuItemRoute = SomeRoute ProfileR
, menuItemModal = False
, menuItemAccessCallback' = isJust <$> maybeAuthPair
}
, return MenuItem
{ menuItemType = NavbarSecondary
{ menuItemType = NavbarSecondary
, menuItemLabel = MsgMenuLogin
, menuItemIcon = Just "sign-in-alt"
, menuItemIcon = Just "sign-in-alt"
, menuItemRoute = SomeRoute $ AuthR LoginR
, menuItemModal = True
, menuItemAccessCallback' = isNothing <$> maybeAuthPair
}
, return MenuItem
{ menuItemType = NavbarSecondary
{ menuItemType = NavbarSecondary
, menuItemLabel = MsgMenuLogout
, menuItemIcon = Just "sign-out-alt"
, menuItemIcon = Just "sign-out-alt"
, menuItemRoute = SomeRoute $ AuthR LogoutR
, menuItemModal = False
, menuItemAccessCallback' = isJust <$> maybeAuthPair
}
, return MenuItem
{ menuItemType = NavbarAside
{ menuItemType = NavbarAside
, menuItemLabel = MsgMenuCourseList
, menuItemIcon = Just "calendar-alt"
, menuItemIcon = Just "graduation-cap"
, menuItemRoute = SomeRoute CourseListR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
, return MenuItem
{ menuItemType = NavbarAside
{ menuItemType = NavbarAside
, menuItemLabel = MsgMenuTermShow
, menuItemIcon = Just "graduation-cap"
, menuItemIcon = Just "calendar-alt" -- SJ wrote: calendar icon, since Term will be repleaced with TimeTable in the future; arguably Term is more calendar-like than courses anyway!!!
, menuItemRoute = SomeRoute TermShowR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
, return MenuItem
{ menuItemType = NavbarAside
{ menuItemType = NavbarAside
, menuItemLabel = MsgMenuCorrections
, menuItemIcon = Just "check"
, menuItemIcon = Just "check"
, menuItemRoute = SomeRoute CorrectionsR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
, return MenuItem
{ menuItemType = NavbarAside
{ menuItemType = NavbarAside
, menuItemLabel = MsgMenuUsers
, menuItemIcon = Just "users"
, menuItemIcon = Just "users"
, menuItemRoute = SomeRoute UsersR
, menuItemModal = False
, menuItemAccessCallback' = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
@ -1184,14 +1203,15 @@ pageActions :: Route UniWorX -> [MenuItem]
-}
pageActions (HomeR) =
[
-- NavbarAside $ MenuItem
-- { menuItemLabel = "Benutzer"
-- , menuItemIcon = Just "users"
-- , menuItemRoute = UsersR
-- , menuItemAccessCallback' = return True
-- }
-- ,
MenuItem
{ menuItemType = PageActionPrime
, menuItemLabel = MsgInfoLecturerTitle
, menuItemIcon = Nothing
, menuItemRoute = SomeRoute InfoLecturerR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
, MenuItem
{ menuItemType = PageActionPrime
, menuItemLabel = MsgMenuAdminTest
, menuItemIcon = Just "screwdriver"
@ -1216,6 +1236,26 @@ pageActions (HomeR) =
, menuItemAccessCallback' = return True
}
]
pageActions (VersionR) = [
MenuItem
{ menuItemType = PageActionPrime
, menuItemLabel = MsgInfoLecturerTitle
, menuItemIcon = Nothing
, menuItemRoute = SomeRoute InfoLecturerR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
]
pageActions (HelpR) = [
MenuItem
{ menuItemType = PageActionPrime
, menuItemLabel = MsgInfoLecturerTitle
, menuItemIcon = Nothing
, menuItemRoute = SomeRoute InfoLecturerR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
]
pageActions (ProfileR) =
[ MenuItem
{ menuItemType = PageActionPrime
@ -1225,14 +1265,14 @@ pageActions (ProfileR) =
, menuItemModal = False
, menuItemAccessCallback' = return True
}
-- , MenuItem
-- { menuItemType = PageActionSecondary
-- , menuItemLabel = MsgMenuAuthPreds
-- , menuItemIcon = Nothing
-- , menuItemRoute = SomeRoute AuthPredsR
-- , menuItemModal = True
-- , menuItemAccessCallback' = return True
-- }
, MenuItem
{ menuItemType = PageActionSecondary
, menuItemLabel = MsgMenuAuthPreds
, menuItemIcon = Nothing
, menuItemRoute = SomeRoute AuthPredsR
, menuItemModal = True
, menuItemAccessCallback' = return True
}
]
pageActions TermShowR =
[ MenuItem
@ -1598,6 +1638,7 @@ pageActions _ = []
i18nHeading :: (MonadWidget m, RenderMessage site msg, HandlerSite m ~ site) => msg -> m ()
i18nHeading msg = liftWidgetT $ toWidget =<< getMessageRender <*> pure msg
-- | only used in defaultLayout; better use siteLayout instead!
pageHeading :: Route UniWorX -> Maybe Widget
pageHeading (AuthR _)
= Just $ i18nHeading MsgLoginHeading
@ -1608,7 +1649,7 @@ pageHeading UsersR
pageHeading (AdminTestR)
= Just $ [whamlet|Internal Code Demonstration Page|]
pageHeading (AdminUserR _)
= Just $ [whamlet|User Display for Admin|]
= Just $ i18nHeading MsgAdminUserHeading
pageHeading (AdminErrMsgR)
= Just $ i18nHeading MsgErrMsgHeading
pageHeading (VersionR)

View File

@ -66,7 +66,7 @@ postAdminTestR = do
_other -> addMessage Warning "KEIN Knopf erkannt"
((emailResult, emailWidget), emailEnctype) <- runFormPost . identifyForm "email" $ renderAForm FormStandard emailTestForm
case emailResult of
case emailResult of
(FormSuccess (email, ls)) -> do
jId <- runDB $ do
jId <- queueJob $ JobSendTestEmail email ls
@ -80,22 +80,12 @@ postAdminTestR = do
<form method=post action=@{AdminTestR} enctype=#{emailEnctype}>
^{emailWidget}
|]
defaultLayout $
-- setTitle "Uni2work Admin Testpage"
$(widgetFile "adminTest")
getAdminUserR :: CryptoUUIDUser -> Handler Html
getAdminUserR uuid = do
uid <- decrypt uuid
User{..} <- runDB $ get404 uid
defaultLayout
[whamlet|
<h1>TODO
<h2>Admin Page for User ^{nameWidget userDisplayName userSurname}
|]
getAdminErrMsgR, postAdminErrMsgR :: Handler Html
getAdminErrMsgR = postAdminErrMsgR
postAdminErrMsgR = do

View File

@ -284,7 +284,7 @@ getCShowR tid ssh csh = do
mRegAt <- traverse (formatTime SelFormatDateTime) registered
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerForm (isJust mRegAt) $ courseRegisterSecret course
registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid ssh csh CRegisterR) True
defaultLayout $ do
siteLayout (toWgt $ courseName course) $ do
setTitle [shamlet| #{toPathPiece tid} - #{csh}|]
$(widgetFile "course")
@ -330,11 +330,14 @@ getCourseNewR = do
<$> iopt termNewField "tid"
<*> iopt ciField "ssh"
<*> iopt ciField "csh"
let noTemplateAction = courseEditHandler True Nothing
case params of -- DO NOT REMOVE: without this distinction, lecturers would never see an empty newCourseForm any more!
let noTemplateAction = courseEditHandler Nothing
case params of -- DO NOT REMOVE: without this distinction, lecturers would never see an empty makeCourseForm any more!
FormMissing -> noTemplateAction
FormFailure msgs -> forM_ msgs (addMessage Error . toHtml) >>
noTemplateAction
FormSuccess (Nothing, Nothing, Nothing) -> noTemplateAction
FormSuccess (fmap TermKey -> mbTid, fmap SchoolKey -> mbSsh, mbCsh) -> do
oldCourses <- runDB $
E.select $ E.from $ \course -> do
@ -378,21 +381,21 @@ getCourseNewR = do
unless cshOk $ addMessageI Warning $ MsgNoSuchCourseShorthand $ fromJust mbCsh
when (tidOk && sshOk && cshOk) $ addMessageI Warning MsgNoSuchCourse
return Nothing
courseEditHandler True template
courseEditHandler template
postCourseNewR :: Handler Html
postCourseNewR = courseEditHandler False Nothing -- Note: Nothing is safe here, since we will create a new course.
postCourseNewR = courseEditHandler Nothing -- Note: Nothing is safe here, since we will create a new course.
getCEditR, postCEditR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCEditR = pgCEditR True
postCEditR = pgCEditR False
getCEditR = pgCEditR
postCEditR = pgCEditR
pgCEditR :: Bool -> TermId -> SchoolId -> CourseShorthand -> Handler Html
pgCEditR isGetReq tid ssh csh = do
pgCEditR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
pgCEditR tid ssh csh = do
course <- runDB $ getBy $ TermSchoolCourseShort tid ssh csh
-- IMPORTANT: both GET and POST Handler must use the same template,
-- since an Edit is identified via CourseID, which is not embedded in the received form data for security reasons.
courseEditHandler isGetReq $ courseToForm <$> course
courseEditHandler $ courseToForm <$> course
getCDeleteR, postCDeleteR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
@ -408,10 +411,10 @@ postCDeleteR tid ssh csh = do
-- | Course Creation and Editing
-- | IMPORTANT: in case of Edit, Post/Get Request is provided with the same CourseForm template (cannot be Nothing),
-- | since an edit is identified via cfCourseId which is not contained in the received form data for security reasons!
courseEditHandler :: Bool -> Maybe CourseForm -> Handler Html -- FIXME: _isGet is not used
courseEditHandler _isGet mbCourseForm = do
courseEditHandler :: Maybe CourseForm -> Handler Html
courseEditHandler mbCourseForm = do
aid <- requireAuthId -- TODO: Verify that Editor is owner of the Course to be Edited!!!
((result, formWidget), formEnctype) <- runFormPost $ newCourseForm mbCourseForm
((result, formWidget), formEnctype) <- runFormPost $ makeCourseForm mbCourseForm
case result of
(FormSuccess res@CourseForm
{ cfCourseId = Nothing
@ -520,8 +523,13 @@ courseToForm (Entity cid Course{..}) = CourseForm
, cfDeRegUntil = courseDeregisterUntil
}
newCourseForm :: Maybe CourseForm -> Form CourseForm
newCourseForm template = identForm FIDcourse $ \html -> do
makeCourseForm :: Maybe CourseForm -> Form CourseForm
makeCourseForm template = identForm FIDcourse $ \html -> do
-- TODO: Refactor to avoid the four repeated calls to liftHandlerT and three runDBs
-- let editCid = cfCourseId =<< template -- possible start for refactoring
mr <- liftHandlerT getMessageRender -- needed for translation of placeholders
userSchools <- liftHandlerT . runDB $ do
userId <- liftHandlerT requireAuthId
(fmap concat . sequence)
@ -529,9 +537,9 @@ newCourseForm template = identForm FIDcourse $ \html -> do
, map (userAdminSchool . entityVal) <$> selectList [UserAdminUser ==. userId] []
]
termsField <- liftHandlerT $ case template of
termsField <- case template of
-- Change of term is only allowed if user may delete the course (i.e. no participants) or admin
(Just cform) | (Just cid) <- cfCourseId cform -> do -- edit existing course
(Just cform) | (Just cid) <- cfCourseId cform -> liftHandlerT $ do -- edit existing course
_courseOld@Course{..} <- runDB $ get404 cid
mayEditTerm <- isAuthorized TermEditR True
mayDelete <- isAuthorized (CourseR courseTerm courseSchool courseShorthand CDeleteR) True
@ -539,29 +547,37 @@ newCourseForm template = identForm FIDcourse $ \html -> do
| (mayEditTerm == Authorized) || (mayDelete == Authorized) -> termsAllowedField
| otherwise -> termsSetField [cfTerm cform]
_allOtherCases -> return termsAllowedField
(newRegFrom,newRegTo,newDeRegUntil) <- case template of
(Just cform) | (Just _cid) <- cfCourseId cform -> return (Nothing,Nothing,Nothing)
_allIOtherCases -> do
mbLastTerm <- liftHandlerT $ runDB $ selectFirst [TermActive ==. True] [Desc TermName]
return ( (Just . toMidnight . termStart . entityVal) <$> mbLastTerm
, (Just . beforeMidnight . termEnd . entityVal) <$> mbLastTerm
, (Just . beforeMidnight . termEnd . entityVal) <$> mbLastTerm )
(result, widget) <- flip (renderAForm FormStandard) html $ CourseForm
<$> pure (cfCourseId =<< template)
<*> areq ciField (fslI MsgCourseName) (cfName <$> template)
<*> areq ciField (fslI MsgCourseName) (cfName <$> template)
<*> aopt htmlField (fslI MsgCourseDescription
& setTooltip MsgCourseDescriptionTip) (cfDesc <$> template)
<*> aopt urlField (fslI MsgCourseHomepage) (cfLink <$> template)
& setTooltip MsgCourseDescriptionTip) (cfDesc <$> template)
<*> aopt urlField (fslI MsgCourseHomepage) (cfLink <$> template)
<*> areq ciField (fslI MsgCourseShorthand
-- & addAttr "disabled" "disabled"
& setTooltip MsgCourseShorthandUnique)
(cfShort <$> template)
<*> areq termsField (fslI MsgCourseSemester) (cfTerm <$> template)
<*> areq (schoolFieldFor userSchools) (fslI MsgCourseSchool) (cfSchool <$> template)
<*> aopt (natField "Kapazität") (fslI MsgCourseCapacity
& setTooltip MsgCourseCapacityTip) (cfCapacity <$> template)
<*> aopt textField (fslpI MsgCourseSecret "beliebige Zeichenkette"
& setTooltip MsgCourseSecretTip) (cfSecret <$> template)
<*> areq checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template)
<*> aopt utcTimeField (fslpI MsgRegisterFrom "Datum"
& setTooltip MsgCourseRegisterFromTip) (cfRegFrom <$> template)
<*> aopt utcTimeField (fslpI MsgRegisterTo "Datum"
& setTooltip MsgCourseRegisterToTip) (cfRegTo <$> template)
<*> aopt utcTimeField (fslpI MsgDeRegUntil "Datum"
& setTooltip MsgCourseDeregisterUntilTip) (cfDeRegUntil <$> template)
& setTooltip MsgCourseShorthandUnique) (cfShort <$> template)
<*> areq termsField (fslI MsgCourseSemester) (cfTerm <$> template)
<*> areq (schoolFieldFor userSchools) (fslI MsgCourseSchool) (cfSchool <$> template)
<*> aopt (natFieldI MsgCourseCapacity) (fslI MsgCourseCapacity
& setTooltip MsgCourseCapacityTip) (cfCapacity <$> template)
<*> aopt textField (fslpI MsgCourseSecret (mr MsgCourseSecretFormat)
& setTooltip MsgCourseSecretTip) (cfSecret <$> template)
<*> areq checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template)
<*> aopt utcTimeField (fslpI MsgRegisterFrom (mr MsgDate)
& setTooltip MsgCourseRegisterFromTip) (deepAlt (cfRegFrom <$> template) newRegFrom)
<*> aopt utcTimeField (fslpI MsgRegisterTo (mr MsgDate)
& setTooltip MsgCourseRegisterToTip) (deepAlt (cfRegTo <$> template) newRegTo)
<*> aopt utcTimeField (fslpI MsgDeRegUntil (mr MsgDate)
& setTooltip MsgCourseDeregisterUntilTip) (deepAlt (cfDeRegUntil <$> template) newDeRegUntil)
<* submitButton
return $ case result of
FormSuccess courseResult

View File

@ -185,7 +185,13 @@ homeUser uid = do
getVersionR :: Handler TypedContent
getVersionR = selectRep $ do
getVersionR = getInfoR -- TODO
getImpressumR :: Handler TypedContent
getImpressumR = getInfoR -- TODO
getInfoR :: Handler TypedContent
getInfoR = selectRep $ do
provideRep . defaultLayout $ do
let features = $(widgetFile "featureList")
gitInfo :: Text
@ -250,11 +256,19 @@ postHelpR = do
tell . pure =<< messageI Success MsgHelpSent
defaultLayout $ do
setTitle "Hilfe" -- TODO: International
setTitleI MsgHelpTitle
isModal <- hasCustomHeader HeaderIsModal
$(widgetFile "help")
getInfoLecturerR :: Handler Html
getInfoLecturerR =
siteLayoutMsg' MsgInfoLecturerTitle $ do
setTitleI MsgInfoLecturerTitle
-- TODO: Translation. This is simply too much for a simple message and too akwward to cut into bits. Create i18nWidgetFile tool.
$(widgetFile "infoLecturer")
getAuthPredsR, postAuthPredsR :: Handler Html
getAuthPredsR = postAuthPredsR
postAuthPredsR = do
@ -281,6 +295,6 @@ postAuthPredsR = do
addMessageI Success MsgAuthPredsActiveChanged
redirect $ fromMaybe AuthPredsR mReferer
defaultLayout $ do
siteLayoutMsg MsgAuthPredsActive $ do
setTitleI MsgAuthPredsActive
$(widgetFile "authpreds")

View File

@ -117,8 +117,8 @@ postProfileR = do
let formText = Nothing :: Maybe UniWorXMessage
actionUrl = ProfileR
defaultLayout $ do
setTitle . toHtml $ userIdent <> "'s User page"
siteLayout [whamlet|_{MsgProfileFor} ^{nameWidget userDisplayName userSurname}|] $ do
setTitle . toHtml $ "Profil " <> userIdent
$(widgetFile "formPageI18n")
postProfileDataR :: Handler Html
@ -190,9 +190,6 @@ deleteUser duid = do
getProfileDataR :: Handler Html
getProfileDataR = do
(uid, User{..}) <- requireAuthPair

View File

@ -8,6 +8,7 @@ import Utils.Lens
import qualified Data.CaseInsensitive as CI
import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Database.Esqueleto as E
@ -16,7 +17,7 @@ import qualified Database.Esqueleto as E
hijackUserForm :: CryptoUUIDUser -> Form ()
hijackUserForm cID csrf = do
(uidResult, uidView) <- mforced hiddenField "" (cID :: CryptoUUIDUser)
(btnResult, btnView) <- mreq (buttonField BtnHijack) "" Nothing
(btnResult, btnView) <- mreq (buttonField BtnHijack) "" Nothing
return (() <$ uidResult <* btnResult, mconcat [toWidget csrf, fvInput uidView, fvInput btnView])
@ -69,7 +70,7 @@ getUsersR = do
]
psValidator = def
& defaultSorting [SortAscBy "name", SortAscBy "display-name"]
((), userList) <- runDB $ dbTable psValidator DBTable
{ dbtSQLQuery = return :: E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (Entity User))
, dbtRowKey = (E.^. UserId)
@ -92,7 +93,7 @@ getUsersR = do
, dbtParams = def
, dbtIdent = "users" :: Text
}
defaultLayout $ do
setTitleI MsgUserListTitle
$(widgetFile "users")
@ -107,3 +108,60 @@ postAdminHijackUserR cID = do
setCredsRedirect $ Creds "dummy" (CI.original userIdent) []
maybe (redirect UsersR) return ret
getAdminUserR, postAdminUserR :: CryptoUUIDUser -> Handler Html
getAdminUserR = postAdminUserR
postAdminUserR uuid = do
adminId <- requireAuthId
uid <- decrypt uuid
let fromSchoolList = Set.fromList . map (userAdminSchool . entityVal)
(User{..}, fromSchoolList -> adminSchools, userRights) <- runDB $ (,,)
<$> get404 uid
<*> selectList [UserAdminUser ==. adminId] []
<*> E.select ( E.from $ \school -> do
E.orderBy [E.asc $ school E.^. SchoolName]
let schAdmin = E.exists $ E.from $ \userAdmin -> do
E.where_ $ userAdmin E.^. UserAdminSchool E.==. school E.^. SchoolId
E.where_ $ userAdmin E.^. UserAdminUser E.==. E.val uid
let schLecturer = E.exists $ E.from $ \userLecturer -> do
E.where_ $ userLecturer E.^. UserLecturerSchool E.==. school E.^. SchoolId
E.where_ $ userLecturer E.^. UserLecturerUser E.==. E.val uid
return (school,schAdmin,schLecturer)
)
-- above data is needed for both form generation and result evaluation
let userRightsForm :: Form [(SchoolId, Bool, Bool)]
userRightsForm csrf = do
boxRights <- forM userRights $ \(school@(Entity sid _), E.Value isAdmin, E.Value isLecturer) ->
if Set.member sid adminSchools
then do
cbAdmin <- mreq checkBoxField "" (Just isAdmin)
cbLecturer <- mreq checkBoxField "" (Just isLecturer)
return (school, cbAdmin, cbLecturer)
else do
cbAdmin <- mforced checkBoxField "" isAdmin
cbLecturer <- mforced checkBoxField "" isLecturer
return (school, cbAdmin, cbLecturer)
let result = forM boxRights $ \(Entity sid _, (resAdmin,_), (resLecturer, _)) ->
(,,) <$> pure sid <*> resAdmin <*> resLecturer
return (result,$(widgetFile "widgets/user-rights-form"))
let userRightsAction changes = do
void . runDB $
forM changes $ \(sid, userAdmin, userLecturer) ->
if Set.notMember sid adminSchools
then return ()
else do
if userAdmin
then void . insertUnique $ UserAdmin uid sid
else deleteBy $ UniqueUserAdmin uid sid
if userLecturer
then void . insertUnique $ UserLecturer uid sid
else deleteBy $ UniqueSchoolLecturer uid sid
-- Note: deleteWhere would not work well here since we filter by adminSchools
addMessageI Info MsgAccessRightsSaved
((result, formWidget),formEnctype) <- runFormPost userRightsForm
formResult result userRightsAction
let heading =
[whamlet|_{MsgAccessRightsFor} ^{nameWidget userDisplayName userSurname}|]
siteLayout heading
$(widgetFile "adminUser")

View File

@ -1,12 +1,13 @@
module Handler.Utils.DateTime
( utcToLocalTime
, localTimeToUTC, TZ.LocalToUTCResult(..)
, toMidnight, beforeMidnight, toMidday, toMorning
, formatTime, formatTime', formatTimeW
, getTimeLocale, getDateTimeFormat
, validDateTimeFormats, dateTimeFormatOptions
, formatTimeMail
, addOneWeek, addWeeks
, weekDiff, weeksToAdd
, weeksToAdd
, setYear
) where
@ -29,6 +30,24 @@ utcToLocalTime = TZ.utcToLocalTimeTZ appTZ
localTimeToUTC :: LocalTime -> LocalToUTCResult
localTimeToUTC = TZ.localTimeToUTCFull appTZ
-- | Local midnight of given day
toMidnight :: Day -> UTCTime
toMidnight d = localTimeToUTCTZ appTZ $ LocalTime d midnight
-- | Local midnight of given day
toMidday :: Day -> UTCTime
toMidday d = localTimeToUTCTZ appTZ $ LocalTime d midday
-- | One second before the end of day
beforeMidnight :: Day -> UTCTime
beforeMidnight d = localTimeToUTCTZ appTZ $ LocalTime d $ TimeOfDay 23 59 59
-- | 6am in the morning
toMorning :: Day -> UTCTime
toMorning d = localTimeToUTCTZ appTZ $ LocalTime d $ TimeOfDay 6 0 0
class FormatTime t => HasLocalTime t where
toLocalTime :: t -> LocalTime
@ -134,36 +153,28 @@ dateTimeFormatOptions sel = do
optionsPairs <=< mapM toOption . Set.toList $ validDateTimeFormats tl sel
setYear :: Integer -> Day -> Day
setYear year date = fromGregorian year month day
where
(_,month,day) = toGregorian date
addOneWeek :: UTCTime -> UTCTime
addOneWeek = addWeeks 1
addWeeks :: Integer -> UTCTime -> UTCTime
addWeeks n utct = utct { utctDay = newDay }
addWeeks n utct = localTimeToUTCTZ appTZ newLocal
where
oldDay = utctDay utct
-- newDay = addGregorianDurationRollOver $ stimes n calendarWeek -- only available in newer version 1.9 of Data.Time.Calendar
newDay = addDays (7*n) oldDay
weekDiff :: UTCTime -> UTCTime -> Integer
-- ^ Difference between times, rounded down to weeks
weekDiff old new = dayDiff `div` 7
where
dayOld = utctDay old
dayNew = utctDay new
dayDiff = diffDays dayNew dayOld
oldLocal = utcToLocalTime utct
oldDay = localDay oldLocal
newDay = addDays (7*n) oldDay
newLocal = oldLocal { localDay = newDay }
weeksToAdd :: UTCTime -> UTCTime -> Integer
-- ^ Number of weeks needed to add so that first
-- time occurs later than second time
-- (loop avoids off-by-one error with weekDiff corner cases)
-- (loop avoids off-by-one error with corner cases)
weeksToAdd old new = loop 0 old
where
loop n t
| t > new = n
| otherwise = loop (succ n) (addOneWeek t)
setYear :: Integer -> Day -> Day
setYear year date = fromGregorian year month day
where
(_,month,day) = toGregorian date
| otherwise = loop (succ n) (addOneWeek t)

View File

@ -135,7 +135,6 @@ buttonForm csrf = do
|])
------------
-- Fields --
------------

View File

@ -708,27 +708,27 @@ pseudonymFragments = folding
$ mapM (toListOf pseudonymWords) . (\l -> guard (length l == 2) *> l) . filter (not . null) . Text.split (\(CI.mk -> c) -> not $ Set.member c pseudonymCharacters)
data AuthTag
= AuthFree
| AuthAdmin
| AuthNoEscalation
| AuthDeprecated
| AuthDevelopment
data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prädikate sind sortier nach Relevanz für Benutzer
= AuthAdmin
| AuthLecturer
| AuthCorrector
| AuthTime
| AuthRegistered
| AuthParticipant
| AuthCapacity
| AuthEmpty
| AuthTime
| AuthMaterials
| AuthOwner
| AuthRated
| AuthUserSubmissions
| AuthCorrectorSubmissions
| AuthCapacity
| AuthEmpty
| AuthAuthentication
| AuthNoEscalation
| AuthRead
| AuthWrite
| AuthDeprecated
| AuthDevelopment
| AuthFree
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
instance Universe AuthTag

View File

@ -99,6 +99,7 @@ data AppSettings = AppSettings
, appNotificationCollateDelay :: NominalDiffTime
, appNotificationExpiration :: NominalDiffTime
, appSessionTimeout :: NominalDiffTime
, appMaximumContentLength :: Maybe Word64
, appInitialLogSettings :: LogSettings
@ -354,6 +355,8 @@ instance FromJSON AppSettings where
appSessionTimeout <- o .: "session-timeout"
appMaximumContentLength <- o .: "maximum-content-length"
appReloadTemplates <- o .:? "reload-templates" .!= defaultDev
appMutableStatic <- o .:? "mutable-static" .!= defaultDev
appSkipCombining <- o .:? "skip-combining" .!= defaultDev

View File

@ -6,6 +6,7 @@ module Settings.StaticFiles
import ClassyPrelude
import Settings (appStaticDir, compileTimeAppSettings)
import Settings.StaticFiles.Generator
import Yesod.EmbeddedStatic
-- This generates easy references to files in the static directory at compile time,
@ -23,4 +24,4 @@ import Yesod.EmbeddedStatic
#define DEV_BOOL False
#endif
mkEmbeddedStatic DEV_BOOL "embeddedStatic" [embedDir $ appStaticDir compileTimeAppSettings]
mkEmbeddedStatic DEV_BOOL "embeddedStatic" . pure . staticGenerator $ appStaticDir compileTimeAppSettings

View File

@ -0,0 +1,72 @@
module Settings.StaticFiles.Generator
( staticGenerator
) where
import ClassyPrelude
import Yesod.EmbeddedStatic.Types
import Yesod.EmbeddedStatic
import System.FilePath
import System.Directory.Tree
import Network.Mime
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Map as Map
import qualified Text.Sass.Compilation as Sass
import Text.Sass.Options
import Data.Default
import qualified Data.Foldable as Fold
staticGenerator :: FilePath -> Generator
staticGenerator staticDir = do
dirTree' <- runIO $ readDirectoryWith toEntries staticDir
Fold.forM_ (fst <$> zipPaths dirTree') addDependentFile
return . Fold.fold $ dirTree dirTree'
where
toEntries :: FilePath -- ^ Absolute path
-> IO [Entry]
toEntries loc = compile (mimeByExt mimeMap defaultMimeType $ pack loc) (makeRelative staticDir loc) loc
mimeMap = defaultMimeMap `mappend` Map.fromList
[ ("sass", "text/x-sass")
, ("scss", "text/x-scss")
]
compile :: MimeType
-> Location -- ^ Relative location
-> FilePath -- ^ Absolute filepath
-> IO [Entry]
compile "text/x-scss" sassLoc fp = return . pure $ def
{ ebHaskellName = Just $ pathToName sassLoc
, ebLocation
, ebMimeType = "text/css"
, ebProductionContent = either (fail <=< Sass.errorMessage) (return . LBS.fromStrict) =<< Sass.compileFile fp def
, ebDevelReload = [| either (fail <=< Sass.errorMessage) (return . LBS.fromStrict) =<< Sass.compileFile $(litE $ stringL fp) def |]
}
where
ebLocation = sassLoc -<.> "css"
compile "text/x-sass" sassLoc fp = return . pure $ def
{ ebHaskellName = Just $ pathToName sassLoc
, ebLocation
, ebMimeType = "text/css"
, ebProductionContent = either (fail <=< Sass.errorMessage) (return . LBS.fromStrict) =<< Sass.compileFile fp (def { sassIsIndentedSyntax = True })
, ebDevelReload = [| either (fail <=< Sass.errorMessage) (return . LBS.fromStrict) =<< Sass.compileFile $(litE $ stringL fp) (def { sassIsIndentedSyntax = True }) |]
}
where
ebLocation = sassLoc -<.> "css"
compile ebMimeType ebLocation fp = return . pure $ def
{ ebHaskellName = Just $ pathToName ebLocation
, ebLocation
, ebMimeType
, ebProductionContent = LBS.fromStrict <$> BS.readFile fp
, ebDevelReload = [| LBS.fromStrict <$> BS.readFile $(litE $ stringL fp) |]
}

View File

@ -326,6 +326,13 @@ maybeAdd (Just x) (Just y) = Just (x + y)
maybeAdd Nothing y = y
maybeAdd x Nothing = x
-- | Deep alternative to avoid any occurrence of Nothing at all costs, left-biased
deepAlt :: Maybe (Maybe a) -> Maybe (Maybe a) -> Maybe (Maybe a)
deepAlt Nothing altSnd = altSnd
deepAlt altFst Nothing = altFst
deepAlt (Just Nothing) altSnd = altSnd
deepAlt altFst _ = altFst
maybeEmpty :: Monoid m => Maybe a -> (a -> m) -> m
maybeEmpty = flip foldMap

View File

@ -336,12 +336,22 @@ combinedButtonFieldF_ :: forall m a p.
) => p a -> FieldSettings (HandlerSite m) -> AForm m ()
combinedButtonFieldF_ _ = void . combinedButtonFieldF @m @a
-- | Submit-Button as AForm, also see submitButtonView below
submitButton :: (Button (HandlerSite m) ButtonSubmit, MonadHandler m) => AForm m ()
submitButton = combinedButtonFieldF_ (Proxy @ButtonSubmit) ""
autosubmitButton :: (Button (HandlerSite m) ButtonSubmit, MonadHandler m) => AForm m ()
autosubmitButton = combinedButtonFieldF_ (Proxy @ButtonSubmit) $ "" & addAutosubmit
-- | just Html for a Submit-Button
submitButtonView :: forall site . Button site ButtonSubmit => WidgetT site IO ()
submitButtonView = do
let bField :: Field (HandlerT site IO) ButtonSubmit
bField = buttonField BtnSubmit
btnId <- newIdent
fieldView bField btnId "" mempty (Right BtnSubmit) False
-------------------
-- Custom Fields --
-------------------
@ -432,7 +442,7 @@ formSection' formSectionTitle = mreq noinputField sectionSettings Nothing
, fsTooltip = Nothing
, fsId = Just idFormSectionNoinput
, fsName = Nothing
, fsAttrs = [("classy",idFormSectionNoinput)] -- TODO: check if this it useful at all
, fsAttrs = [("class",idFormSectionNoinput)] -- TODO: check if this it useful at all
}

View File

@ -156,7 +156,7 @@
}
}
.alert__close {
.alert__closer {
cursor: pointer;
text-align: right;
position: absolute;
@ -196,7 +196,7 @@
@media (max-width: 768px) {
.alert__close {
.alert__closer {
width: 40px;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 99 KiB

86
static/js/utils/alerts.js Normal file
View File

@ -0,0 +1,86 @@
(function() {
'use strict';
window.utils = window.utils || {};
var ALERTS_TOGGLER_CLASS = 'alerts__toggler';
var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible';
var ALERTS_TOGGLER_APPEAR_DELAY = 120;
var ALERT_CLASS = 'alert';
var ALERT_CLOSER_CLASS = 'alert__closer';
var ALERT_INVISIBLE_CLASS = 'alert--invisible';
var ALERT_AUTO_HIDE_DELAY = 10;
var ALERT_AUTOCLOSING_MATCHER = '.alert-info, .alert-success';
var JS_INITIALIZED_CLASS = 'js-initialized';
window.utils.alerts = function(alertsEl) {
if (alertsEl.classList.contains(JS_INITIALIZED_CLASS)) {
return;
}
var togglerCheckRequested = false;
var togglerEl = alertsEl.querySelector('.' + ALERTS_TOGGLER_CLASS);
var alertElements = Array.from(alertsEl.querySelectorAll('.' + ALERT_CLASS))
.filter(function(alert) {
return !alert.classList.contains(JS_INITIALIZED_CLASS);
});
function initToggler() {
togglerEl.addEventListener('click', function() {
alertElements.forEach(function(alertEl) {
toggleAlert(alertEl, true);
});
togglerEl.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS);
});
alertsEl.classList.add(JS_INITIALIZED_CLASS);
}
function initAlert(alertEl) {
var autoHideDelay = ALERT_AUTO_HIDE_DELAY;
if (alertEl.dataset.decay) {
autoHideDelay = parseInt(alertEl.dataset.decay, 10);
}
var closeEl = alertEl.querySelector('.' + ALERT_CLOSER_CLASS);
closeEl.addEventListener('click', function() {
toggleAlert(alertEl);
});
if (autoHideDelay > 0 && alertEl.matches(ALERT_AUTOCLOSING_MATCHER)) {
window.setTimeout(function() {
toggleAlert(alertEl);
}, autoHideDelay * 1000);
}
alertEl.classList.add(JS_INITIALIZED_CLASS);
}
function toggleAlert(alertEl, visible) {
alertEl.classList.toggle(ALERT_INVISIBLE_CLASS, !visible);
checkToggler();
}
function checkToggler() {
if (togglerCheckRequested) {
return;
}
var alertsHidden = alertElements.reduce(function(acc, alert) {
return acc && alert.classList.contains(ALERT_INVISIBLE_CLASS);
}, true);
window.setTimeout(function() {
togglerEl.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden);
togglerCheckRequested = false;
}, ALERTS_TOGGLER_APPEAR_DELAY);
}
initToggler();
alertElements.forEach(initAlert);
};
})();

View File

@ -0,0 +1,3 @@
<form method=post action=@{AdminUserR uuid} enctype=#{formEnctype}>
^{formWidget}
^{submitButtonView}

View File

@ -1,3 +1,4 @@
_{MsgAuthPredsInfo}
<form method=post action=@{AuthPredsR} enctype=#{authActiveEnctype}>
$maybe referer <- mReferer
<input type=hidden name=#{toPathPiece GetReferer} value=#{toPathPiece referer}>

View File

@ -30,12 +30,7 @@ $if not isModal
^{widget}
<!-- alerts -->
<div #alerts .alerts>
$forall (status, msg) <- mmsgs
$with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}">
<div .alert__content>
#{msg}
^{alerts}
<!-- footer -->
$if not isModal

View File

@ -262,6 +262,12 @@ button,
a:hover {
color: white;
}
&:focus {
border-color: #3273dc;
box-shadow: 0 0 0 0.25rem rgba(50,115,220,.25);
outline: 0;
}
}
input[type="submit"][disabled],
@ -327,7 +333,7 @@ input[type="button"].btn-info:hover,
/* SCROLLTABLE */
.scrolltable {
overflow: auto;
box-shadow: 0 0 3px 0 var(--color-grey);
box-shadow: 0 0 1px 1px var(--color-grey-light);
margin-bottom: 15px;
}

View File

@ -0,0 +1,113 @@
UniWorX erfahrene Veranstalter finden
hier die wichtigsten Neuerungen.
<section>
<h2>Veranstaltungen
<dl .deflist>
<dt .deflist__dt> Kurskürzel
<dd .deflist__dd>
Alle Veranstaltungen müssen ein Kürzel zur Identifikation besitzen,
z.B. EiP, ProMo, SysPrak, etc.
<br>
Das Kürzel muss innerhalb Institut und Semester eindeutig sein.
<dt .deflist__dt> Kurse klonen
<dd .deflist__dd>
Veranstalter können <em>alle</em> Kurse Ihres Instituts für das aktuelle Semesters klonen.
<br>
Dabei werden vor allem Kurskürzel und die Kursbeschreibung übernommen;
nicht jedoch Übungsblätter, Klausuren oder Anmeldungen.
<br>
Die Kursbeschreibung kann in Html verfasst werden und
<em>sollte die Modulbeschreibung enthalten!
<dt .deflist__dt> Passwort
<dd .deflist__dd> Die Anmeldung zum Kurs kann durch ein Passwort geschützt werden.
<dt .deflist__dt> Materialzugriff
<dd .deflist__dd>
Der Zugriff auf Übungsblätter, Folien und andere Materialien
kann von der Anmeldung zum Kurs abhängig gemacht werden.
<dt .deflist__dt> Teilnehmer
<dd .deflist__dd>
Anzeige und Benachrichtigung angemeldeter
Teilnehmer ist leider noch nicht fertig implementiert.
Voraussichtlich noch vor Start des Sommersemesters 2019 verfügbar.
<section>
<h2>Übungsbetrieb
<dl .deflist>
<dt .deflist__dt> Korrektoren
<dd .deflist__dd>
Korrektoren und Korrekturweise werden ad hoc pro Übungsblatt vom Veranstalter festgelegt;
es gibt keine Eintragung von Korrektoren in der Veranstaltungskonfiguration mehr.
Um reinen Tutoren vorab Zugriff auf Lösungen zu gewähren, sollten diese
als Korrektoren mit Korrekturanteil 0 eingetragen werden.
Für ein neues Blatt werden die Einstellung des vorangegangenen
Blattes automatisch vorgeschlagen.
<dt .deflist__dt> Verteilung
<dd .deflist__dd>
Korrektoren können pro Blatt auch als Abwesend oder Entschuldigt
markiert werden und bekommen dann keine Abgaben zugeteilt.
Abwesende Korrektoren bekommen in späteren Blättern
mehr Abgaben zugeteilt, entsprechend ihres
Anteils am verpassten Blatt.
<dt .deflist__dt> Dateien
<dd .deflist__dd>
Ein einzelnes Übungsblatt kann nun aus mehreren Dateien beliebigen Typs
für Angabe und Lösung bestehen.
Im Gegensatz zu den bisher verpflichtenden Zip-Archiven
können nun einzelne Dateien in einem Schritt verändert werden.
<dt .deflist__dt> Lösungshinweise
<dd .deflist__dd>
Zusätzlich zu Aufgabe und Lösung können Hinweise ab einem
Datum vor Abgabfrist freigeschaltet werden,
z.B. Lösungen zu Präsenzaufgaben.
<dt .deflist__dt> Sichtbarkeit
<dd .deflist__dd>
Übungsblätter können bis zu einem Datum vor den Teilnehmern versteckt werden.
<dt .deflist__dt> Zeitstempel
<dd .deflist__dd>
Alle Dateien eines Übungsblattes sind mit einem
für Teilnehmer sichtbaren Zeitstempel versehen.
<br>
Eine visuelle Hervorhebung geänderter/neuer Dateien
und entsprechende Benachrichtigungen sind geplant,
aber noch nicht verfügbar.
<dt .deflist__dt> Übungsgruppen
<dd .deflist__dd>
Eine Anmeldung zu Übungsgruppen wie bisher
ist leider noch nicht fertig implementiert.
<dt .deflist__dt> Papierabgaben
<dd .deflist__dd>
Abgaben in anderer Form (z.B. Papierabgaben)
können mit Hilfe von Tokens verwaltet werden.
Korrekturen können elektronisch zurückgegeben werden.
<section>
<h2>Klausuren
Das Verwalten von Klausuren und Notenmeldungen
ist leider noch nicht fertig implementiert.
<section>
<h2>Sonstiges
<dl .deflist>
<dt .deflist__dt> Wartung
<dd .deflist__dd>
Planmäßige Wartungen werden ohne Ankündigung
immer um 2:00h nachts durchgeführt.
Es wird daher empfohlen, keine kritischen Abgabefristen
um oder kurz nach dieser Zeit einzustellen.

View File

@ -117,9 +117,8 @@
<h4>Hinweise:
<ul>
<li>
Sichern Sie Ihre Daten!
Alle Daten des Systems werden nach Abschluss des Testbetriebs von Uni2work
unwiderruflich gelöscht werden! (Voraussichtlich ein paar Wochen vor Beginn des Wintersemesters 18/19, spätestens aber im Dezember 2018.)
Sichern Sie Ihre Daten! Während des Testbetriebs von Uni2work
könnten Daten unabsichtlich unwidderuflich gelöscht werden.
<li>
Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrektur von Übungen, Übungsgruppenleiterschaft, Raumbuchungen, etc.
<li>

View File

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

View File

@ -1,101 +0,0 @@
(function() {
'use strict';
window.utils = window.utils || {};
var ALERT_INVISIBLE_CLASS = 'alert--invisible';
var TOGGLER_INVISIBLE_CLASS = 'alerts__toggler--visible';
var ALERT_AUTO_DISAPPEAR_DELAY = 10;
var alertsShowingToggler = false;
window.utils.alerts = function(alertsEl) {
var toggler = alertsEl.querySelector('.alerts__toggler');
function makeToggler() {
toggler = document.createElement('DIV');
toggler.classList.add('alerts__toggler');
toggler.addEventListener('click', function() {
Array.from(alertsEl.querySelectorAll('.alert')).forEach(function(alert) {
alert.classList.remove(ALERT_INVISIBLE_CLASS);
toggler.classList.remove(TOGGLER_INVISIBLE_CLASS);
});
checkToggler();
});
alertsEl.appendChild(toggler);
alertsEl.classList.add('js-initialized');
}
function makeAlert(alertEl) {
var iconEl = document.createElement('DIV');
var closeEl = document.createElement('DIV');
var dataDecay = alertEl.dataset.decay;
var autoDecay = ALERT_AUTO_DISAPPEAR_DELAY;
if (dataDecay) {
autoDecay = parseInt(dataDecay, 10);
}
iconEl.classList.add('alert__icon');
closeEl.classList.add('alert__close');
closeEl.addEventListener('click', function(event) {
closeAlert(alertEl);
});
alertEl.insertBefore(iconEl, alertEl.children[0]);
alertEl.insertBefore(closeEl, alertEl.children[0]);
// auto-hide info and success-alerts after 3 seconds
if (autoDecay > 0 && !alertEl.matches('.alert-warning, .alert-error')) {
window.setTimeout(function() {
closeAlert(alertEl);
}, autoDecay * 1000);
}
alertEl.classList.add('js-initialized');
}
function closeAlert(alertEl) {
alertEl.classList.add(ALERT_INVISIBLE_CLASS);
checkToggler();
}
function checkToggler() {
var hidden = true;
Array.from(alertsEl.querySelectorAll('.alert')).forEach(function(alert) {
if (hidden && !alert.classList.contains(ALERT_INVISIBLE_CLASS)) {
hidden = false;
}
});
if (!alertsShowingToggler) {
alertsShowingToggler = true;
window.setTimeout(function() {
toggler.classList.toggle(TOGGLER_INVISIBLE_CLASS, hidden);
alertsShowingToggler = false;
}, 120);
}
}
if (!alertsEl.classList.contains('js-initialized') || !toggler)
makeToggler();
Array.from(alertsEl.querySelectorAll('.alert:not(.js-initialized)')).map(makeAlert);
}
})();
document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'alerts')
return;
// setup alerts
if (e.detail.scope.classList.contains('alerts')) {
window.utils.alerts(e.detail.scope);
} else {
var alertsEl = e.detail.scope.querySelector('.alerts');
if (alertsEl)
window.utils.alerts(alertsEl);
}
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'alerts' }, bubbles: true, cancelable: true }))
});

View File

@ -85,7 +85,8 @@ input[type="url"],
input[type="number"],
input[type="email"],
input[type*="date"],
input[type*="time"] {
input[type*="time"],
select {
/* from bulma.css */
color: #363636;
border-color: #dbdbdb;
@ -102,25 +103,6 @@ input[type*="time"] {
font-family: var(--font-base);
line-height: 1.5;
padding: 4px 13px;
&:focus {
border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0;
}
&[disabled] {
background-color: #f5f5f5;
color: #7a7a7a;
box-shadow: none;
border-color: #dbdbdb;
}
&[readonly] {
background-color: #f5f5f5;
box-shadow: none;
border-color: #dbdbdb;
}
}
input[type="number"] {
@ -153,7 +135,13 @@ textarea {
border-radius: 2px;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
vertical-align: top;
}
/* SHARED STATE RELATED STYLES */
input,
select,
textarea {
&:focus {
border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
@ -169,7 +157,6 @@ textarea {
&[readonly] {
background-color: #f5f5f5;
box-shadow: none;
border-color: #dbdbdb;
}
}
@ -240,7 +227,12 @@ input[type="checkbox"]:checked::after {
[type="checkbox"],
[type="radio"] {
display: none;
position: fixed;
top: -1px;
left: -1px;
width: 1px;
height: 1px;
overflow: hidden;
}
label {
@ -270,6 +262,13 @@ input[type="checkbox"]:checked::after {
background-color: var(--color-primary);
}
[type="checkbox"]:focus + label,
[type="radio"]:focus + label {
border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0;
}
:checked + label::before,
:checked + label::after {
content: '';

View File

@ -13,7 +13,9 @@
color: var(--color-font);
padding: 0 65px 0 20px;
overflow: auto;
transition: all .15s ease;
transition:
opacity .2s .1s ease-in-out,
transform .3s ease-in-out;
pointer-events: none;
opacity: 0;

View File

@ -5,7 +5,7 @@
window.utils.asyncTable = function(wrapper, options) {
var tableIdent = #{String dbtIdent};
var tableIdent = wrapper.dataset.dbtIdent;
var shortCircuitHeader = #{String (toPathPiece HeaderDBTableShortcircuit)};
var ths = [];
@ -14,7 +14,7 @@
var scrollTable;
function init() {
var table = wrapper.querySelector('#' + tableIdent);
var table = wrapper.querySelector('#' + tableIdent);
if (!table) {
return;
}
@ -38,8 +38,8 @@
pagesizeForm = wrapper.querySelector('#' + tableIdent + '-pagesize-form');
// take options into account
if (options && options.scrollToTop) {
window.scrollTo(0, 0);
if (options && options.scrollTo) {
window.scrollTo(options.scrollTo);
}
if (options && options.horizPos && scrollTable) {
@ -63,13 +63,22 @@
pageLinks.forEach(function(link) {
link.clickHandler = function(event) {
var boundClickHandler = clickHandler.bind(this);
boundClickHandler(event, { scrollToTop: true });
var wrapperBoundingRect = wrapper.getBoundingClientRect();
var options = {};
if (wrapperBoundingRect.top < 160) {
options.scrollTo = {
top: (wrapper.offsetTop || 0) - 60,
left: wrapper.offsetLeft || 0,
behavior: 'smooth',
};
}
boundClickHandler(event, options);
}
link.element.addEventListener('click', link.clickHandler);
});
if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]')
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]');
pagesizeSelect.addEventListener('change', changePagesizeHandler);
}
}
@ -167,9 +176,11 @@
})();
document.addEventListener('DOMContentLoaded', function() {
var selector = '#' + #{String $ dbtIdent} + '-table-wrapper:not(.js-initialized)';
var dbtIdent = #{String $ dbtIdent};
var selector = '#' + dbtIdent + '-table-wrapper:not(.js-initialized)';
var wrapper = document.querySelector(selector);
if (wrapper) {
wrapper.dataset.dbtIdent = dbtIdent;
window.utils.asyncTable(wrapper);
}
});

View File

@ -18,7 +18,7 @@
.page-link {
margin-top: 7px;
display: inline-block;
background-color: var(--color-grey);
background-color: var(--color-grey-medium);
+ .page-link {
margin-left: 7px;
@ -28,10 +28,11 @@
color: var(--color-lightwhite);
padding: 7px 13px;
display: inline-block;
text-decoration: none;
}
&:not(.current):hover {
background-color: var(--color-lighter);
background-color: var(--color-primary);
a {
color: var(--color-lightwhite);
@ -40,10 +41,9 @@
&.current {
pointer-events: none;
background-color: var(--color-light);
background-color: var(--color-dark);
a {
text-decoration: underline;
pointer-events: none;
}
}

View File

@ -0,0 +1,9 @@
<div #alerts .alerts>
<div .alerts__toggler>
$forall (status, msg) <- mmsgs
$with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}">
<div .alert__closer>
<div .alert__icon>
<div .alert__content>
#{msg}

View File

@ -0,0 +1,18 @@
document.addEventListener('setup', function(e) {
if (!e.detail.module || e.detail.module !== 'alerts') {
return;
}
// setup alerts
if (e.detail.scope.classList.contains('alerts')) {
window.utils.alerts(e.detail.scope);
} else {
var alertsEl = e.detail.scope.querySelector('.alerts');
if (alertsEl)
window.utils.alerts(alertsEl);
}
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'alerts' }, bubbles: true, cancelable: true }))
});

View File

@ -127,14 +127,14 @@
flex-basis: var(--asidenav-width-xl, 250px);
font-size: 16px;
align-items: center;
color: var(--color-dark);
color: var(--color-primary);
transform-origin: left;
&:hover {
color: var(--color-dark);
color: var(--color-lightwhite);
.asidenav__logo-link-item {
background-color: var(--color-grey-light);
background-color: var(--color-primary);
}
}
}
@ -145,7 +145,7 @@
align-items: flex-end;
height: calc(100% - 4px);
padding: 0 6px 4px;
border: 1px solid var(--color-dark);
border: 1px solid var(--color-primary);
letter-spacing: 2px;
background-color: var(--color-lightwhite);
transition: background-color .3s ease;

View File

@ -36,10 +36,7 @@
top: 0;
left: var(--asidenav-width-xl);
height: var(--header-height);
background: var(--color-darker); /* Old browsers */
background: -moz-linear-gradient(bottom, var(--color-primary) 0%, var(--color-primary) 80%, var(--color-dark) 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(bottom, var(--color-primary) 0%, var(--color-primary) 80%, var(--color-dark) 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(to top, var(--color-primary) 0%, var(--color-primary) 80%, var(--color-dark) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
background-color: var(--color-primary);
color: white;
z-index: 20;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
@ -221,7 +218,7 @@
}
.navbar .navbar__list-item:not(.navbar__list-item--active):not(.navbar__list-item--favorite):hover .navbar__link-wrapper {
background-color: var(--color-darker);
background-color: var(--color-dark);
color: var(--color-lightwhite);
}

View File

@ -0,0 +1,14 @@
$newline never
#{csrf}
<div .scrolltable>
<table .table .table--striped .table--hover>
<tr .table__row .table__row--head>
<th>
$# empty cell
<th .table__th>_{MsgAdminFor}
<th .table__th>_{MsgLecturerFor}
$forall (Entity _ (School name _), (_,cbAdmin), (_,cbLecturer)) <- boxRights
<tr .table__row>
<th .table__th>#{name}
<td .table__td>^{fvInput cbAdmin}
<td .table__td>^{fvInput cbLecturer}