merged from master
This commit is contained in:
commit
76ac563627
@ -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
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
BIN
assets/favicon-5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 557 B |
4
assets/logo-o2.svg
Normal file
4
assets/logo-o2.svg
Normal 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
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -114,6 +114,7 @@ dependencies:
|
||||
- memcached-binary
|
||||
- directory-tree
|
||||
- lifted-base
|
||||
- hsass
|
||||
|
||||
other-extensions:
|
||||
- GeneralizedNewtypeDeriving
|
||||
|
||||
18
routes
18
routes
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
@ -135,7 +135,6 @@ buttonForm csrf = do
|
||||
|])
|
||||
|
||||
|
||||
|
||||
------------
|
||||
-- Fields --
|
||||
------------
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
72
src/Settings/StaticFiles/Generator.hs
Normal file
72
src/Settings/StaticFiles/Generator.hs
Normal 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) |]
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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
86
static/js/utils/alerts.js
Normal 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);
|
||||
};
|
||||
})();
|
||||
3
templates/adminUser.hamlet
Normal file
3
templates/adminUser.hamlet
Normal file
@ -0,0 +1,3 @@
|
||||
<form method=post action=@{AdminUserR uuid} enctype=#{formEnctype}>
|
||||
^{formWidget}
|
||||
^{submitButtonView}
|
||||
@ -1,3 +1,4 @@
|
||||
_{MsgAuthPredsInfo}
|
||||
<form method=post action=@{AuthPredsR} enctype=#{authActiveEnctype}>
|
||||
$maybe referer <- mReferer
|
||||
<input type=hidden name=#{toPathPiece GetReferer} value=#{toPathPiece referer}>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
113
templates/infoLecturer.hamlet
Normal file
113
templates/infoLecturer.hamlet
Normal 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.
|
||||
@ -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>
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<!-- only here to be able to include alerts using `toWidget` -->
|
||||
@ -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 }))
|
||||
});
|
||||
@ -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: '';
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
9
templates/widgets/alerts/alerts.hamlet
Normal file
9
templates/widgets/alerts/alerts.hamlet
Normal 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}
|
||||
18
templates/widgets/alerts/alerts.julius
Normal file
18
templates/widgets/alerts/alerts.julius
Normal 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 }))
|
||||
});
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
14
templates/widgets/user-rights-form.hamlet
Normal file
14
templates/widgets/user-rights-form.hamlet
Normal 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}
|
||||
Reference in New Issue
Block a user