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 * Version 30.01.2019
Designänderungen Designänderungen
* Version 16.01.2019 * Version 16.01.2019
@ -7,7 +7,7 @@
Links für Bequemlichkeiten hinzugefügt (z.B. aktuelles Übungsblatt) Links für Bequemlichkeiten hinzugefügt (z.B. aktuelles Übungsblatt)
Liste zugewiesener Abgaben lassen sich nun filtern Liste zugewiesener Abgaben lassen sich nun filtern
Bugfix: Wenn zwischen Anzeige und Empfang eines Tabellen-Formulars Zeilen verschwinden wird nun eine sinnvolle Fehlermeldung angezeigt Bugfix: Wenn zwischen Anzeige und Empfang eines Tabellen-Formulars Zeilen verschwinden wird nun eine sinnvolle Fehlermeldung angezeigt
* Version 30.11.2018 * Version 30.11.2018
@ -65,11 +65,6 @@
Bugfixes, wählbares Format für Datum 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 * Version 03.07.2018
Willkommen bei Uni2work aka "You-need-to-work!" 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-collate-delay: 300
notification-expiration: 259201 notification-expiration: 259201
session-timeout: 7200 session-timeout: 7200
maximum-content-length: 52428800
log-settings: log-settings:
detailed: "_env:DETAILED_LOGGING:false" detailed: "_env:DETAILED_LOGGING:false"

View File

@ -47,7 +47,7 @@ LectureStart: Beginn Vorlesungen
Course: Kurs Course: Kurs
CourseShort: Kürzel CourseShort: Kürzel
CourseCapacity: Kapazität 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. CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei.
CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet. CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet.
CourseRegisterOk: Sie wurden angemeldet CourseRegisterOk: Sie wurden angemeldet
@ -75,14 +75,15 @@ CourseDescription: Beschreibung
CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet
CourseHomepage: Homepage CourseHomepage: Homepage
CourseShorthand: Kürzel CourseShorthand: Kürzel
CourseShorthandUnique: Muss innerhalb des Semesters eindeutig sein CourseShorthandUnique: Muss innerhalb Institut und Semester eindeutig sein
CourseSemester: Semester CourseSemester: Semester
CourseSchool: Institut CourseSchool: Institut
CourseSchoolShort: Fach CourseSchoolShort: Fach
CourseSecretTip: Anmeldung zum Kurs erfordert Eingabe des Passworts, sofern gesetzt 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 CourseRegisterFromTip: Ohne Datum ist KEINE eigenständige Anmeldung von Studierenden möglich
CourseRegisterToTip: Anmeldung darf auch ohne Begrenzung möglich sein CourseRegisterToTip: Anmeldung darf auch unbegrenzt offen bleiben
CourseDeregisterUntilTip: Abmeldung darf auch ohne Begrenzung möglich sein CourseDeregisterUntilTip: Abmeldung darf auch unbegrenzt erlaubt bleiben
CourseFilterSearch: Volltext-Suche CourseFilterSearch: Volltext-Suche
CourseFilterRegistered: Registriert CourseFilterRegistered: Registriert
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen? CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
@ -231,6 +232,7 @@ HomeHeading: Aktuelle Termine
LoginHeading: Authentifizierung LoginHeading: Authentifizierung
LoginTitle: Authentifizierung LoginTitle: Authentifizierung
ProfileHeading: Benutzereinstellungen ProfileHeading: Benutzereinstellungen
ProfileFor: Benutzereinstellungen für
ProfileDataHeading: Gespeicherte Benutzerdaten ProfileDataHeading: Gespeicherte Benutzerdaten
ImpressumHeading: Impressum ImpressumHeading: Impressum
SystemMessageHeading: Uni2work Statusmeldung SystemMessageHeading: Uni2work Statusmeldung
@ -333,11 +335,15 @@ MultiSinkException name@Text error@Text: In Abgabe #{name} ist ein Fehler aufget
NoTableContent: Kein Tabelleninhalt NoTableContent: Kein Tabelleninhalt
NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter
AdminUserHeading: Benutzeradministration
AccessRightsFor: Berechtigungen für
AdminFor: Administrator AdminFor: Administrator
LecturerFor: Dozent LecturerFor: Dozent
LecturersFor: Dozenten LecturersFor: Dozenten
UserListTitle: Komprehensive Benutzerliste UserListTitle: Komprehensive Benutzerliste
AccessRightsSaved: Berechtigungsänderungen wurden gespeichert.
Date: Datum
DateTimeFormat: Datums- und Uhrzeitformat DateTimeFormat: Datums- und Uhrzeitformat
DateFormat: Datumsformat DateFormat: Datumsformat
TimeFormat: Uhrzeitformat TimeFormat: Uhrzeitformat
@ -489,6 +495,7 @@ CorrGrade: Korrekturen eintragen
UserAccountDeleted name@Text: Konto für #{name} wurde gelöscht! UserAccountDeleted name@Text: Konto für #{name} wurde gelöscht!
HelpTitle : Hilfe
HelpAnswer: Antworten an HelpAnswer: Antworten an
HelpUser: Meinen Benutzeraccount HelpUser: Meinen Benutzeraccount
HelpAnonymous: Keine Antwort (Anonym) 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. 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. HelpSent: Ihre Supportanfrage wurde weitergeleitet.
InfoLecturerTitle: Hinweise für Veranstalter
SystemMessageFrom: Sichtbar ab SystemMessageFrom: Sichtbar ab
SystemMessageTo: Sichtbar bis SystemMessageTo: Sichtbar bis
SystemMessageAuthenticatedOnly: Nur angemeldet SystemMessageAuthenticatedOnly: Nur angemeldet
@ -560,7 +570,8 @@ ErrorCryptoIdMismatch: Verschlüsselte Id der Abgabe passte nicht zu anderen Dat
InvalidRoute: Konnte URL nicht interpretieren InvalidRoute: Konnte URL nicht interpretieren
MenuHome: Aktuell MenuHome: Aktuell
MenuVersion: Impressum MenuImpressum: Impressum
MenuVersion: Versionsgeschichte
MenuHelp: Hilfe MenuHelp: Hilfe
MenuProfile: Anpassen MenuProfile: Anpassen
MenuLogin: Login MenuLogin: Login
@ -598,6 +609,7 @@ MenuCorrectionsCreate: Abgaben registrieren
MenuCorrectionsGrade: Abgaben bewerten MenuCorrectionsGrade: Abgaben bewerten
MenuAuthPreds: Authorisierungseinstellungen 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 AuthPredsActive: Aktive Authorisierungsprädikate
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
AuthTagFree: Seite ist universell zugänglich AuthTagFree: Seite ist universell zugänglich

View File

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

18
routes
View File

@ -11,7 +11,7 @@
-- --
-- Access Tags: -- Access Tags:
-- !free -- free for all -- !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) -- !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) -- !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) -- !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 / HomeR GET !free
/users UsersR GET -- no tags, i.e. admins only /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 /users/#CryptoUUIDUser/hijack AdminHijackUserR POST !adminANDno-escalation
/admin/test AdminTestR GET POST /admin/test AdminTestR GET POST
/admin/errMsg AdminErrMsgR 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 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 TermShowR GET !free
/term/current TermCurrentR GET !free /term/current TermCurrentR GET !free
/term/edit TermEditR GET POST /term/edit TermEditR GET POST
/term/#TermId/edit TermEditExistR GET /term/#TermId/edit TermEditExistR GET
!/term/#TermId TermCourseListR GET !free !/term/#TermId TermCourseListR GET !free
!/term/#TermId/#SchoolId TermSchoolCourseListR GET !free !/term/#TermId/#SchoolId TermSchoolCourseListR GET !free
/school SchoolListR GET !development /school SchoolListR GET !development
/school/#SchoolId SchoolShowR 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)) return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c>0) (unauthorizedI MsgUnauthorizedLecturer) guardMExceptT (c>0) (unauthorizedI MsgUnauthorizedLecturer)
return Authorized return Authorized
-- lecturer for any school will do
_ -> exceptT return return $ do _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId authId <- maybeExceptT AuthenticationRequired $ lift maybeAuthId
void . maybeMExceptT (unauthorizedI MsgUnauthorizedSchoolLecturer) $ selectFirst [UserLecturerUser ==. authId] [] void . maybeMExceptT (unauthorizedI MsgUnauthorizedSchoolLecturer) $ selectFirst [UserLecturerUser ==. authId] []
@ -752,7 +753,7 @@ instance Yesod UniWorX where
(getCachedDate, _) <- clientSessionDateCacher appSessionTimeout (getCachedDate, _) <- clientSessionDateCacher appSessionTimeout
return . Just $ clientSessionBackend appSessionKey getCachedDate 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. -- 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. -- 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}|] NotAuthenticated -> [whamlet|<p>_{MsgErrorResponseNotAuthenticated}|]
PermissionDenied err' -> [whamlet|<p .errMsg>#{err'}|] PermissionDenied err' -> [whamlet|<p .errMsg>#{err'}|]
BadMethod method -> [whamlet|<p>_{MsgErrorResponseBadMethod (decodeUtf8 method)}|] 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 toWidget
[cassius| [cassius|
.errMsg .errMsg
@ -844,7 +845,7 @@ instance Yesod UniWorX where
|] |]
errPage errPage
defaultLayout = siteLayout Nothing defaultLayout = siteLayout' Nothing
-- The page to be redirected to when authentication is required. -- The page to be redirected to when authentication is required.
authRoute _ = Just $ AuthR LoginR authRoute _ = Just $ AuthR LoginR
@ -887,9 +888,21 @@ instance Yesod UniWorX where
makeLogger = readTVarIO . snd . appLogger 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 -> 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 master <- getYesod
let AppSettings { appUserDefaults = UserDefaultConf{..}, .. } = appSettings master let AppSettings { appUserDefaults = UserDefaultConf{..}, .. } = appSettings master
@ -961,8 +974,10 @@ siteLayout headingOverride widget = do
asidenav = $(widgetFile "widgets/asidenav") asidenav = $(widgetFile "widgets/asidenav")
footer :: Widget footer :: Widget
footer = $(widgetFile "widgets/footer") footer = $(widgetFile "widgets/footer")
alerts :: Widget
alerts = $(widgetFile "widgets/alerts/alerts")
contentHeadline :: Maybe Widget contentHeadline :: Maybe Widget
contentHeadline = (toWidget <$> headingOverride) <|> (pageHeading =<< mcurrentRoute) contentHeadline = headingOverride <|> (pageHeading =<< mcurrentRoute)
breadcrumbsWgt :: Widget breadcrumbsWgt :: Widget
breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs") breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs")
pageaction :: Widget pageaction :: Widget
@ -974,23 +989,24 @@ siteLayout headingOverride widget = do
hasPrimaryPageActions = any (is _PageActionPrime) $ toListOf (traverse . _1 . _menuItemType) menuTypes hasPrimaryPageActions = any (is _PageActionPrime) $ toListOf (traverse . _1 . _menuItemType) menuTypes
pc <- widgetToPageContent $ do pc <- widgetToPageContent $ do
addScript $ StaticR js_zepto_js addScript $ StaticR js_vendor_zepto_js
addScript $ StaticR js_fetchPolyfill_js addScript $ StaticR js_vendor_flatpickr_js
addScript $ StaticR js_urlPolyfill_js addScript $ StaticR js_polyfills_fetchPolyfill_js
addScript $ StaticR js_featureChecker_js addScript $ StaticR js_polyfills_urlPolyfill_js
addScript $ StaticR js_flatpickr_js addScript $ StaticR js_utils_featureChecker_js
addScript $ StaticR js_tabber_js addScript $ StaticR js_utils_tabber_js
addStylesheet $ StaticR css_flatpickr_css addScript $ StaticR js_utils_alerts_js
addStylesheet $ StaticR css_tabber_css addStylesheet $ StaticR css_vendor_flatpickr_css
addStylesheet $ StaticR css_vendor_fontawesome_css
addStylesheet $ StaticR css_fonts_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 "default-layout")
$(widgetFile "standalone/modal") $(widgetFile "standalone/modal")
$(widgetFile "standalone/showHide") $(widgetFile "standalone/showHide")
$(widgetFile "standalone/inputs") $(widgetFile "standalone/inputs")
$(widgetFile "standalone/tooltip") $(widgetFile "standalone/tooltip")
$(widgetFile "standalone/tabber") $(widgetFile "standalone/tabber")
$(widgetFile "standalone/alerts")
$(widgetFile "standalone/datepicker") $(widgetFile "standalone/datepicker")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
@ -1022,25 +1038,28 @@ applySystemMessages = liftHandlerT . runDB . runConduit $ selectSource [] [] .|
-- Define breadcrumbs. -- Define breadcrumbs.
instance YesodBreadcrumbs UniWorX where instance YesodBreadcrumbs UniWorX where
breadcrumb (AuthR _) = return ("Login" , Just HomeR) breadcrumb (AuthR _) = return ("Login" , Just HomeR)
breadcrumb HomeR = return ("Uni2work", Nothing) breadcrumb HomeR = return ("Uni2work" , Nothing)
breadcrumb UsersR = return ("Benutzer", Just HomeR) breadcrumb UsersR = return ("Benutzer" , Just HomeR)
breadcrumb AdminTestR = return ("Test" , Just HomeR) breadcrumb AdminTestR = return ("Test" , Just HomeR)
breadcrumb (AdminUserR _) = return ("Users" , Just UsersR) breadcrumb (AdminUserR _) = return ("Users" , Just UsersR)
breadcrumb VersionR = return ("Impressum" , Just HomeR) 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 ProfileR = return ("User" , Just HomeR)
breadcrumb ProfileDataR = return ("Data" , Just ProfileR) breadcrumb ProfileDataR = return ("Profile" , Just ProfileR)
breadcrumb AuthPredsR = return ("Authentifizierung", Just ProfileR)
breadcrumb TermShowR = return ("Semester" , Just HomeR) breadcrumb TermShowR = return ("Semester" , Just HomeR)
breadcrumb TermCurrentR = return ("Aktuell" , Just TermShowR) breadcrumb TermCurrentR = return ("Aktuell" , Just TermShowR)
breadcrumb TermEditR = return ("Neu" , Just TermCurrentR) breadcrumb TermEditR = return ("Neu" , Just TermCurrentR)
breadcrumb (TermEditExistR tid) = return ("Editieren" , Just $ TermCourseListR tid) 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 (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 CourseNewR = return ("Neu" , Just CourseListR)
breadcrumb (CourseR tid ssh csh CShowR) = return (CI.original csh, Just $ TermSchoolCourseListR tid ssh) breadcrumb (CourseR tid ssh csh CShowR) = return (CI.original csh, Just $ TermSchoolCourseListR tid ssh)
-- (CourseR tid ssh csh CRegisterR) -- is POST only -- (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 :: (MonadHandler m, HandlerSite m ~ UniWorX) => m [MenuItem]
defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header. defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header.
[ return MenuItem [ return MenuItem
{ menuItemType = NavbarAside { menuItemType = NavbarAside
, menuItemLabel = MsgMenuHome , menuItemLabel = MsgMenuHome
, menuItemIcon = Just "home" , menuItemIcon = Just "home"
, menuItemRoute = SomeRoute HomeR , menuItemRoute = SomeRoute HomeR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
, return MenuItem , return MenuItem
{ menuItemType = Footer { menuItemType = Footer
, menuItemLabel = MsgMenuVersion , menuItemLabel = MsgMenuImpressum
, menuItemIcon = Just "book" , menuItemIcon = Just "book"
, menuItemRoute = SomeRoute VersionR , menuItemRoute = SomeRoute ImpressumR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
@ -1109,7 +1128,7 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
mCurrentRoute <- getCurrentRoute mCurrentRoute <- getCurrentRoute
return MenuItem return MenuItem
{ menuItemType = NavbarRight { menuItemType = NavbarRight
, menuItemLabel = MsgMenuHelp , menuItemLabel = MsgMenuHelp
, menuItemIcon = Just "question" , menuItemIcon = Just "question"
, menuItemRoute = SomeRoute (HelpR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mCurrentRoute]) , 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 , menuItemAccessCallback' = return True
} }
, return MenuItem , return MenuItem
{ menuItemType = NavbarRight { menuItemType = NavbarRight
, menuItemLabel = MsgMenuProfile , menuItemLabel = MsgMenuProfile
, menuItemIcon = Just "cogs" , menuItemIcon = Just "cogs"
, menuItemRoute = SomeRoute ProfileR , menuItemRoute = SomeRoute ProfileR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = isJust <$> maybeAuthPair , menuItemAccessCallback' = isJust <$> maybeAuthPair
} }
, return MenuItem , return MenuItem
{ menuItemType = NavbarSecondary { menuItemType = NavbarSecondary
, menuItemLabel = MsgMenuLogin , menuItemLabel = MsgMenuLogin
, menuItemIcon = Just "sign-in-alt" , menuItemIcon = Just "sign-in-alt"
, menuItemRoute = SomeRoute $ AuthR LoginR , menuItemRoute = SomeRoute $ AuthR LoginR
, menuItemModal = True , menuItemModal = True
, menuItemAccessCallback' = isNothing <$> maybeAuthPair , menuItemAccessCallback' = isNothing <$> maybeAuthPair
} }
, return MenuItem , return MenuItem
{ menuItemType = NavbarSecondary { menuItemType = NavbarSecondary
, menuItemLabel = MsgMenuLogout , menuItemLabel = MsgMenuLogout
, menuItemIcon = Just "sign-out-alt" , menuItemIcon = Just "sign-out-alt"
, menuItemRoute = SomeRoute $ AuthR LogoutR , menuItemRoute = SomeRoute $ AuthR LogoutR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = isJust <$> maybeAuthPair , menuItemAccessCallback' = isJust <$> maybeAuthPair
} }
, return MenuItem , return MenuItem
{ menuItemType = NavbarAside { menuItemType = NavbarAside
, menuItemLabel = MsgMenuCourseList , menuItemLabel = MsgMenuCourseList
, menuItemIcon = Just "calendar-alt" , menuItemIcon = Just "graduation-cap"
, menuItemRoute = SomeRoute CourseListR , menuItemRoute = SomeRoute CourseListR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
, return MenuItem , return MenuItem
{ menuItemType = NavbarAside { menuItemType = NavbarAside
, menuItemLabel = MsgMenuTermShow , 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 , menuItemRoute = SomeRoute TermShowR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
, return MenuItem , return MenuItem
{ menuItemType = NavbarAside { menuItemType = NavbarAside
, menuItemLabel = MsgMenuCorrections , menuItemLabel = MsgMenuCorrections
, menuItemIcon = Just "check" , menuItemIcon = Just "check"
, menuItemRoute = SomeRoute CorrectionsR , menuItemRoute = SomeRoute CorrectionsR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
, return MenuItem , return MenuItem
{ menuItemType = NavbarAside { menuItemType = NavbarAside
, menuItemLabel = MsgMenuUsers , menuItemLabel = MsgMenuUsers
, menuItemIcon = Just "users" , menuItemIcon = Just "users"
, menuItemRoute = SomeRoute UsersR , menuItemRoute = SomeRoute UsersR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False , menuItemAccessCallback' = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
@ -1184,14 +1203,15 @@ pageActions :: Route UniWorX -> [MenuItem]
-} -}
pageActions (HomeR) = pageActions (HomeR) =
[ [
-- NavbarAside $ MenuItem
-- { menuItemLabel = "Benutzer"
-- , menuItemIcon = Just "users"
-- , menuItemRoute = UsersR
-- , menuItemAccessCallback' = return True
-- }
-- ,
MenuItem MenuItem
{ menuItemType = PageActionPrime
, menuItemLabel = MsgInfoLecturerTitle
, menuItemIcon = Nothing
, menuItemRoute = SomeRoute InfoLecturerR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
, MenuItem
{ menuItemType = PageActionPrime { menuItemType = PageActionPrime
, menuItemLabel = MsgMenuAdminTest , menuItemLabel = MsgMenuAdminTest
, menuItemIcon = Just "screwdriver" , menuItemIcon = Just "screwdriver"
@ -1216,6 +1236,26 @@ pageActions (HomeR) =
, menuItemAccessCallback' = return True , 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) = pageActions (ProfileR) =
[ MenuItem [ MenuItem
{ menuItemType = PageActionPrime { menuItemType = PageActionPrime
@ -1225,14 +1265,14 @@ pageActions (ProfileR) =
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
-- , MenuItem , MenuItem
-- { menuItemType = PageActionSecondary { menuItemType = PageActionSecondary
-- , menuItemLabel = MsgMenuAuthPreds , menuItemLabel = MsgMenuAuthPreds
-- , menuItemIcon = Nothing , menuItemIcon = Nothing
-- , menuItemRoute = SomeRoute AuthPredsR , menuItemRoute = SomeRoute AuthPredsR
-- , menuItemModal = True , menuItemModal = True
-- , menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
-- } }
] ]
pageActions TermShowR = pageActions TermShowR =
[ MenuItem [ MenuItem
@ -1598,6 +1638,7 @@ pageActions _ = []
i18nHeading :: (MonadWidget m, RenderMessage site msg, HandlerSite m ~ site) => msg -> m () i18nHeading :: (MonadWidget m, RenderMessage site msg, HandlerSite m ~ site) => msg -> m ()
i18nHeading msg = liftWidgetT $ toWidget =<< getMessageRender <*> pure msg i18nHeading msg = liftWidgetT $ toWidget =<< getMessageRender <*> pure msg
-- | only used in defaultLayout; better use siteLayout instead!
pageHeading :: Route UniWorX -> Maybe Widget pageHeading :: Route UniWorX -> Maybe Widget
pageHeading (AuthR _) pageHeading (AuthR _)
= Just $ i18nHeading MsgLoginHeading = Just $ i18nHeading MsgLoginHeading
@ -1608,7 +1649,7 @@ pageHeading UsersR
pageHeading (AdminTestR) pageHeading (AdminTestR)
= Just $ [whamlet|Internal Code Demonstration Page|] = Just $ [whamlet|Internal Code Demonstration Page|]
pageHeading (AdminUserR _) pageHeading (AdminUserR _)
= Just $ [whamlet|User Display for Admin|] = Just $ i18nHeading MsgAdminUserHeading
pageHeading (AdminErrMsgR) pageHeading (AdminErrMsgR)
= Just $ i18nHeading MsgErrMsgHeading = Just $ i18nHeading MsgErrMsgHeading
pageHeading (VersionR) pageHeading (VersionR)

View File

@ -66,7 +66,7 @@ postAdminTestR = do
_other -> addMessage Warning "KEIN Knopf erkannt" _other -> addMessage Warning "KEIN Knopf erkannt"
((emailResult, emailWidget), emailEnctype) <- runFormPost . identifyForm "email" $ renderAForm FormStandard emailTestForm ((emailResult, emailWidget), emailEnctype) <- runFormPost . identifyForm "email" $ renderAForm FormStandard emailTestForm
case emailResult of case emailResult of
(FormSuccess (email, ls)) -> do (FormSuccess (email, ls)) -> do
jId <- runDB $ do jId <- runDB $ do
jId <- queueJob $ JobSendTestEmail email ls jId <- queueJob $ JobSendTestEmail email ls
@ -80,22 +80,12 @@ postAdminTestR = do
<form method=post action=@{AdminTestR} enctype=#{emailEnctype}> <form method=post action=@{AdminTestR} enctype=#{emailEnctype}>
^{emailWidget} ^{emailWidget}
|] |]
defaultLayout $ defaultLayout $
-- setTitle "Uni2work Admin Testpage" -- setTitle "Uni2work Admin Testpage"
$(widgetFile "adminTest") $(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 :: Handler Html
getAdminErrMsgR = postAdminErrMsgR getAdminErrMsgR = postAdminErrMsgR
postAdminErrMsgR = do postAdminErrMsgR = do

View File

@ -284,7 +284,7 @@ getCShowR tid ssh csh = do
mRegAt <- traverse (formatTime SelFormatDateTime) registered mRegAt <- traverse (formatTime SelFormatDateTime) registered
(regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerForm (isJust mRegAt) $ courseRegisterSecret course (regWidget, regEnctype) <- generateFormPost $ identifyForm "registerBtn" $ registerForm (isJust mRegAt) $ courseRegisterSecret course
registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid ssh csh CRegisterR) True registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid ssh csh CRegisterR) True
defaultLayout $ do siteLayout (toWgt $ courseName course) $ do
setTitle [shamlet| #{toPathPiece tid} - #{csh}|] setTitle [shamlet| #{toPathPiece tid} - #{csh}|]
$(widgetFile "course") $(widgetFile "course")
@ -330,11 +330,14 @@ getCourseNewR = do
<$> iopt termNewField "tid" <$> iopt termNewField "tid"
<*> iopt ciField "ssh" <*> iopt ciField "ssh"
<*> iopt ciField "csh" <*> 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 FormMissing -> noTemplateAction
FormFailure msgs -> forM_ msgs (addMessage Error . toHtml) >> FormFailure msgs -> forM_ msgs (addMessage Error . toHtml) >>
noTemplateAction noTemplateAction
FormSuccess (Nothing, Nothing, Nothing) -> noTemplateAction
FormSuccess (fmap TermKey -> mbTid, fmap SchoolKey -> mbSsh, mbCsh) -> do FormSuccess (fmap TermKey -> mbTid, fmap SchoolKey -> mbSsh, mbCsh) -> do
oldCourses <- runDB $ oldCourses <- runDB $
E.select $ E.from $ \course -> do E.select $ E.from $ \course -> do
@ -378,21 +381,21 @@ getCourseNewR = do
unless cshOk $ addMessageI Warning $ MsgNoSuchCourseShorthand $ fromJust mbCsh unless cshOk $ addMessageI Warning $ MsgNoSuchCourseShorthand $ fromJust mbCsh
when (tidOk && sshOk && cshOk) $ addMessageI Warning MsgNoSuchCourse when (tidOk && sshOk && cshOk) $ addMessageI Warning MsgNoSuchCourse
return Nothing return Nothing
courseEditHandler True template courseEditHandler template
postCourseNewR :: Handler Html 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, postCEditR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCEditR = pgCEditR True getCEditR = pgCEditR
postCEditR = pgCEditR False postCEditR = pgCEditR
pgCEditR :: Bool -> TermId -> SchoolId -> CourseShorthand -> Handler Html pgCEditR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
pgCEditR isGetReq tid ssh csh = do pgCEditR tid ssh csh = do
course <- runDB $ getBy $ TermSchoolCourseShort tid ssh csh course <- runDB $ getBy $ TermSchoolCourseShort tid ssh csh
-- IMPORTANT: both GET and POST Handler must use the same template, -- 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. -- 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 getCDeleteR, postCDeleteR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
@ -408,10 +411,10 @@ postCDeleteR tid ssh csh = do
-- | Course Creation and Editing -- | Course Creation and Editing
-- | IMPORTANT: in case of Edit, Post/Get Request is provided with the same CourseForm template (cannot be Nothing), -- | 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! -- | 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 :: Maybe CourseForm -> Handler Html
courseEditHandler _isGet mbCourseForm = do courseEditHandler mbCourseForm = do
aid <- requireAuthId -- TODO: Verify that Editor is owner of the Course to be Edited!!! 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 case result of
(FormSuccess res@CourseForm (FormSuccess res@CourseForm
{ cfCourseId = Nothing { cfCourseId = Nothing
@ -520,8 +523,13 @@ courseToForm (Entity cid Course{..}) = CourseForm
, cfDeRegUntil = courseDeregisterUntil , cfDeRegUntil = courseDeregisterUntil
} }
newCourseForm :: Maybe CourseForm -> Form CourseForm makeCourseForm :: Maybe CourseForm -> Form CourseForm
newCourseForm template = identForm FIDcourse $ \html -> do 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 userSchools <- liftHandlerT . runDB $ do
userId <- liftHandlerT requireAuthId userId <- liftHandlerT requireAuthId
(fmap concat . sequence) (fmap concat . sequence)
@ -529,9 +537,9 @@ newCourseForm template = identForm FIDcourse $ \html -> do
, map (userAdminSchool . entityVal) <$> selectList [UserAdminUser ==. userId] [] , 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 -- 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 _courseOld@Course{..} <- runDB $ get404 cid
mayEditTerm <- isAuthorized TermEditR True mayEditTerm <- isAuthorized TermEditR True
mayDelete <- isAuthorized (CourseR courseTerm courseSchool courseShorthand CDeleteR) True mayDelete <- isAuthorized (CourseR courseTerm courseSchool courseShorthand CDeleteR) True
@ -539,29 +547,37 @@ newCourseForm template = identForm FIDcourse $ \html -> do
| (mayEditTerm == Authorized) || (mayDelete == Authorized) -> termsAllowedField | (mayEditTerm == Authorized) || (mayDelete == Authorized) -> termsAllowedField
| otherwise -> termsSetField [cfTerm cform] | otherwise -> termsSetField [cfTerm cform]
_allOtherCases -> return termsAllowedField _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 (result, widget) <- flip (renderAForm FormStandard) html $ CourseForm
<$> pure (cfCourseId =<< template) <$> pure (cfCourseId =<< template)
<*> areq ciField (fslI MsgCourseName) (cfName <$> template) <*> areq ciField (fslI MsgCourseName) (cfName <$> template)
<*> aopt htmlField (fslI MsgCourseDescription <*> aopt htmlField (fslI MsgCourseDescription
& setTooltip MsgCourseDescriptionTip) (cfDesc <$> template) & setTooltip MsgCourseDescriptionTip) (cfDesc <$> template)
<*> aopt urlField (fslI MsgCourseHomepage) (cfLink <$> template) <*> aopt urlField (fslI MsgCourseHomepage) (cfLink <$> template)
<*> areq ciField (fslI MsgCourseShorthand <*> areq ciField (fslI MsgCourseShorthand
-- & addAttr "disabled" "disabled" -- & addAttr "disabled" "disabled"
& setTooltip MsgCourseShorthandUnique) & setTooltip MsgCourseShorthandUnique) (cfShort <$> template)
(cfShort <$> template) <*> areq termsField (fslI MsgCourseSemester) (cfTerm <$> template)
<*> areq termsField (fslI MsgCourseSemester) (cfTerm <$> template) <*> areq (schoolFieldFor userSchools) (fslI MsgCourseSchool) (cfSchool <$> template)
<*> areq (schoolFieldFor userSchools) (fslI MsgCourseSchool) (cfSchool <$> template) <*> aopt (natFieldI MsgCourseCapacity) (fslI MsgCourseCapacity
<*> aopt (natField "Kapazität") (fslI MsgCourseCapacity & setTooltip MsgCourseCapacityTip) (cfCapacity <$> template)
& setTooltip MsgCourseCapacityTip) (cfCapacity <$> template) <*> aopt textField (fslpI MsgCourseSecret (mr MsgCourseSecretFormat)
<*> aopt textField (fslpI MsgCourseSecret "beliebige Zeichenkette" & setTooltip MsgCourseSecretTip) (cfSecret <$> template)
& setTooltip MsgCourseSecretTip) (cfSecret <$> template) <*> areq checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template)
<*> areq checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template) <*> aopt utcTimeField (fslpI MsgRegisterFrom (mr MsgDate)
<*> aopt utcTimeField (fslpI MsgRegisterFrom "Datum" & setTooltip MsgCourseRegisterFromTip) (deepAlt (cfRegFrom <$> template) newRegFrom)
& setTooltip MsgCourseRegisterFromTip) (cfRegFrom <$> template) <*> aopt utcTimeField (fslpI MsgRegisterTo (mr MsgDate)
<*> aopt utcTimeField (fslpI MsgRegisterTo "Datum" & setTooltip MsgCourseRegisterToTip) (deepAlt (cfRegTo <$> template) newRegTo)
& setTooltip MsgCourseRegisterToTip) (cfRegTo <$> template) <*> aopt utcTimeField (fslpI MsgDeRegUntil (mr MsgDate)
<*> aopt utcTimeField (fslpI MsgDeRegUntil "Datum" & setTooltip MsgCourseDeregisterUntilTip) (deepAlt (cfDeRegUntil <$> template) newDeRegUntil)
& setTooltip MsgCourseDeregisterUntilTip) (cfDeRegUntil <$> template)
<* submitButton <* submitButton
return $ case result of return $ case result of
FormSuccess courseResult FormSuccess courseResult

View File

@ -185,7 +185,13 @@ homeUser uid = do
getVersionR :: Handler TypedContent getVersionR :: Handler TypedContent
getVersionR = selectRep $ do getVersionR = getInfoR -- TODO
getImpressumR :: Handler TypedContent
getImpressumR = getInfoR -- TODO
getInfoR :: Handler TypedContent
getInfoR = selectRep $ do
provideRep . defaultLayout $ do provideRep . defaultLayout $ do
let features = $(widgetFile "featureList") let features = $(widgetFile "featureList")
gitInfo :: Text gitInfo :: Text
@ -250,11 +256,19 @@ postHelpR = do
tell . pure =<< messageI Success MsgHelpSent tell . pure =<< messageI Success MsgHelpSent
defaultLayout $ do defaultLayout $ do
setTitle "Hilfe" -- TODO: International setTitleI MsgHelpTitle
isModal <- hasCustomHeader HeaderIsModal isModal <- hasCustomHeader HeaderIsModal
$(widgetFile "help") $(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 :: Handler Html
getAuthPredsR = postAuthPredsR getAuthPredsR = postAuthPredsR
postAuthPredsR = do postAuthPredsR = do
@ -281,6 +295,6 @@ postAuthPredsR = do
addMessageI Success MsgAuthPredsActiveChanged addMessageI Success MsgAuthPredsActiveChanged
redirect $ fromMaybe AuthPredsR mReferer redirect $ fromMaybe AuthPredsR mReferer
defaultLayout $ do siteLayoutMsg MsgAuthPredsActive $ do
setTitleI MsgAuthPredsActive setTitleI MsgAuthPredsActive
$(widgetFile "authpreds") $(widgetFile "authpreds")

View File

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

View File

@ -8,6 +8,7 @@ import Utils.Lens
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Set as Set
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
@ -16,7 +17,7 @@ import qualified Database.Esqueleto as E
hijackUserForm :: CryptoUUIDUser -> Form () hijackUserForm :: CryptoUUIDUser -> Form ()
hijackUserForm cID csrf = do hijackUserForm cID csrf = do
(uidResult, uidView) <- mforced hiddenField "" (cID :: CryptoUUIDUser) (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]) return (() <$ uidResult <* btnResult, mconcat [toWidget csrf, fvInput uidView, fvInput btnView])
@ -69,7 +70,7 @@ getUsersR = do
] ]
psValidator = def psValidator = def
& defaultSorting [SortAscBy "name", SortAscBy "display-name"] & defaultSorting [SortAscBy "name", SortAscBy "display-name"]
((), userList) <- runDB $ dbTable psValidator DBTable ((), userList) <- runDB $ dbTable psValidator DBTable
{ dbtSQLQuery = return :: E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (Entity User)) { dbtSQLQuery = return :: E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (Entity User))
, dbtRowKey = (E.^. UserId) , dbtRowKey = (E.^. UserId)
@ -92,7 +93,7 @@ getUsersR = do
, dbtParams = def , dbtParams = def
, dbtIdent = "users" :: Text , dbtIdent = "users" :: Text
} }
defaultLayout $ do defaultLayout $ do
setTitleI MsgUserListTitle setTitleI MsgUserListTitle
$(widgetFile "users") $(widgetFile "users")
@ -107,3 +108,60 @@ postAdminHijackUserR cID = do
setCredsRedirect $ Creds "dummy" (CI.original userIdent) [] setCredsRedirect $ Creds "dummy" (CI.original userIdent) []
maybe (redirect UsersR) return ret 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 module Handler.Utils.DateTime
( utcToLocalTime ( utcToLocalTime
, localTimeToUTC, TZ.LocalToUTCResult(..) , localTimeToUTC, TZ.LocalToUTCResult(..)
, toMidnight, beforeMidnight, toMidday, toMorning
, formatTime, formatTime', formatTimeW , formatTime, formatTime', formatTimeW
, getTimeLocale, getDateTimeFormat , getTimeLocale, getDateTimeFormat
, validDateTimeFormats, dateTimeFormatOptions , validDateTimeFormats, dateTimeFormatOptions
, formatTimeMail , formatTimeMail
, addOneWeek, addWeeks , addOneWeek, addWeeks
, weekDiff, weeksToAdd , weeksToAdd
, setYear , setYear
) where ) where
@ -29,6 +30,24 @@ utcToLocalTime = TZ.utcToLocalTimeTZ appTZ
localTimeToUTC :: LocalTime -> LocalToUTCResult localTimeToUTC :: LocalTime -> LocalToUTCResult
localTimeToUTC = TZ.localTimeToUTCFull appTZ 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 class FormatTime t => HasLocalTime t where
toLocalTime :: t -> LocalTime toLocalTime :: t -> LocalTime
@ -134,36 +153,28 @@ dateTimeFormatOptions sel = do
optionsPairs <=< mapM toOption . Set.toList $ validDateTimeFormats tl sel 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 :: UTCTime -> UTCTime
addOneWeek = addWeeks 1 addOneWeek = addWeeks 1
addWeeks :: Integer -> UTCTime -> UTCTime addWeeks :: Integer -> UTCTime -> UTCTime
addWeeks n utct = utct { utctDay = newDay } addWeeks n utct = localTimeToUTCTZ appTZ newLocal
where where
oldDay = utctDay utct oldLocal = utcToLocalTime utct
-- newDay = addGregorianDurationRollOver $ stimes n calendarWeek -- only available in newer version 1.9 of Data.Time.Calendar oldDay = localDay oldLocal
newDay = addDays (7*n) oldDay newDay = addDays (7*n) oldDay
newLocal = oldLocal { localDay = newDay }
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
weeksToAdd :: UTCTime -> UTCTime -> Integer weeksToAdd :: UTCTime -> UTCTime -> Integer
-- ^ Number of weeks needed to add so that first -- ^ Number of weeks needed to add so that first
-- time occurs later than second time -- 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 weeksToAdd old new = loop 0 old
where where
loop n t loop n t
| t > new = n | t > new = n
| otherwise = loop (succ n) (addOneWeek t) | otherwise = loop (succ n) (addOneWeek t)
setYear :: Integer -> Day -> Day
setYear year date = fromGregorian year month day
where
(_,month,day) = toGregorian date

View File

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

View File

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

View File

@ -6,6 +6,7 @@ module Settings.StaticFiles
import ClassyPrelude import ClassyPrelude
import Settings (appStaticDir, compileTimeAppSettings) import Settings (appStaticDir, compileTimeAppSettings)
import Settings.StaticFiles.Generator
import Yesod.EmbeddedStatic import Yesod.EmbeddedStatic
-- This generates easy references to files in the static directory at compile time, -- This generates easy references to files in the static directory at compile time,
@ -23,4 +24,4 @@ import Yesod.EmbeddedStatic
#define DEV_BOOL False #define DEV_BOOL False
#endif #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 Nothing y = y
maybeAdd x Nothing = x 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 :: Monoid m => Maybe a -> (a -> m) -> m
maybeEmpty = flip foldMap maybeEmpty = flip foldMap

View File

@ -336,12 +336,22 @@ combinedButtonFieldF_ :: forall m a p.
) => p a -> FieldSettings (HandlerSite m) -> AForm m () ) => p a -> FieldSettings (HandlerSite m) -> AForm m ()
combinedButtonFieldF_ _ = void . combinedButtonFieldF @m @a combinedButtonFieldF_ _ = void . combinedButtonFieldF @m @a
-- | Submit-Button as AForm, also see submitButtonView below
submitButton :: (Button (HandlerSite m) ButtonSubmit, MonadHandler m) => AForm m () submitButton :: (Button (HandlerSite m) ButtonSubmit, MonadHandler m) => AForm m ()
submitButton = combinedButtonFieldF_ (Proxy @ButtonSubmit) "" submitButton = combinedButtonFieldF_ (Proxy @ButtonSubmit) ""
autosubmitButton :: (Button (HandlerSite m) ButtonSubmit, MonadHandler m) => AForm m () autosubmitButton :: (Button (HandlerSite m) ButtonSubmit, MonadHandler m) => AForm m ()
autosubmitButton = combinedButtonFieldF_ (Proxy @ButtonSubmit) $ "" & addAutosubmit 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 -- -- Custom Fields --
------------------- -------------------
@ -432,7 +442,7 @@ formSection' formSectionTitle = mreq noinputField sectionSettings Nothing
, fsTooltip = Nothing , fsTooltip = Nothing
, fsId = Just idFormSectionNoinput , fsId = Just idFormSectionNoinput
, fsName = Nothing , 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; cursor: pointer;
text-align: right; text-align: right;
position: absolute; position: absolute;
@ -196,7 +196,7 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.alert__close { .alert__closer {
width: 40px; 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}> <form method=post action=@{AuthPredsR} enctype=#{authActiveEnctype}>
$maybe referer <- mReferer $maybe referer <- mReferer
<input type=hidden name=#{toPathPiece GetReferer} value=#{toPathPiece referer}> <input type=hidden name=#{toPathPiece GetReferer} value=#{toPathPiece referer}>

View File

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

View File

@ -262,6 +262,12 @@ button,
a:hover { a:hover {
color: white; color: white;
} }
&:focus {
border-color: #3273dc;
box-shadow: 0 0 0 0.25rem rgba(50,115,220,.25);
outline: 0;
}
} }
input[type="submit"][disabled], input[type="submit"][disabled],
@ -327,7 +333,7 @@ input[type="button"].btn-info:hover,
/* SCROLLTABLE */ /* SCROLLTABLE */
.scrolltable { .scrolltable {
overflow: auto; overflow: auto;
box-shadow: 0 0 3px 0 var(--color-grey); box-shadow: 0 0 1px 1px var(--color-grey-light);
margin-bottom: 15px; 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: <h4>Hinweise:
<ul> <ul>
<li> <li>
Sichern Sie Ihre Daten! Sichern Sie Ihre Daten! Während des Testbetriebs von Uni2work
Alle Daten des Systems werden nach Abschluss des Testbetriebs von Uni2work könnten Daten unabsichtlich unwidderuflich gelöscht werden.
unwiderruflich gelöscht werden! (Voraussichtlich ein paar Wochen vor Beginn des Wintersemesters 18/19, spätestens aber im Dezember 2018.)
<li> <li>
Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrektur von Übungen, Übungsgruppenleiterschaft, Raumbuchungen, etc. Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrektur von Übungen, Übungsgruppenleiterschaft, Raumbuchungen, etc.
<li> <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="number"],
input[type="email"], input[type="email"],
input[type*="date"], input[type*="date"],
input[type*="time"] { input[type*="time"],
select {
/* from bulma.css */ /* from bulma.css */
color: #363636; color: #363636;
border-color: #dbdbdb; border-color: #dbdbdb;
@ -102,25 +103,6 @@ input[type*="time"] {
font-family: var(--font-base); font-family: var(--font-base);
line-height: 1.5; line-height: 1.5;
padding: 4px 13px; 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"] { input[type="number"] {
@ -153,7 +135,13 @@ textarea {
border-radius: 2px; border-radius: 2px;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05); box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
vertical-align: top; vertical-align: top;
}
/* SHARED STATE RELATED STYLES */
input,
select,
textarea {
&:focus { &:focus {
border-color: #3273dc; border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25); box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
@ -169,7 +157,6 @@ textarea {
&[readonly] { &[readonly] {
background-color: #f5f5f5; background-color: #f5f5f5;
box-shadow: none;
border-color: #dbdbdb; border-color: #dbdbdb;
} }
} }
@ -240,7 +227,12 @@ input[type="checkbox"]:checked::after {
[type="checkbox"], [type="checkbox"],
[type="radio"] { [type="radio"] {
display: none; position: fixed;
top: -1px;
left: -1px;
width: 1px;
height: 1px;
overflow: hidden;
} }
label { label {
@ -270,6 +262,13 @@ input[type="checkbox"]:checked::after {
background-color: var(--color-primary); 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::before,
:checked + label::after { :checked + label::after {
content: ''; content: '';

View File

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

View File

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

View File

@ -18,7 +18,7 @@
.page-link { .page-link {
margin-top: 7px; margin-top: 7px;
display: inline-block; display: inline-block;
background-color: var(--color-grey); background-color: var(--color-grey-medium);
+ .page-link { + .page-link {
margin-left: 7px; margin-left: 7px;
@ -28,10 +28,11 @@
color: var(--color-lightwhite); color: var(--color-lightwhite);
padding: 7px 13px; padding: 7px 13px;
display: inline-block; display: inline-block;
text-decoration: none;
} }
&:not(.current):hover { &:not(.current):hover {
background-color: var(--color-lighter); background-color: var(--color-primary);
a { a {
color: var(--color-lightwhite); color: var(--color-lightwhite);
@ -40,10 +41,9 @@
&.current { &.current {
pointer-events: none; pointer-events: none;
background-color: var(--color-light); background-color: var(--color-dark);
a { a {
text-decoration: underline;
pointer-events: none; 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); flex-basis: var(--asidenav-width-xl, 250px);
font-size: 16px; font-size: 16px;
align-items: center; align-items: center;
color: var(--color-dark); color: var(--color-primary);
transform-origin: left; transform-origin: left;
&:hover { &:hover {
color: var(--color-dark); color: var(--color-lightwhite);
.asidenav__logo-link-item { .asidenav__logo-link-item {
background-color: var(--color-grey-light); background-color: var(--color-primary);
} }
} }
} }
@ -145,7 +145,7 @@
align-items: flex-end; align-items: flex-end;
height: calc(100% - 4px); height: calc(100% - 4px);
padding: 0 6px 4px; padding: 0 6px 4px;
border: 1px solid var(--color-dark); border: 1px solid var(--color-primary);
letter-spacing: 2px; letter-spacing: 2px;
background-color: var(--color-lightwhite); background-color: var(--color-lightwhite);
transition: background-color .3s ease; transition: background-color .3s ease;

View File

@ -36,10 +36,7 @@
top: 0; top: 0;
left: var(--asidenav-width-xl); left: var(--asidenav-width-xl);
height: var(--header-height); height: var(--header-height);
background: var(--color-darker); /* Old browsers */ background-color: var(--color-primary);
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+ */
color: white; color: white;
z-index: 20; z-index: 20;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); 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 { .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); 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}