diff --git a/ChangeLog.md b/ChangeLog.md index 3e3d9dfe8..59d7755a2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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!" diff --git a/assets/favicon-0.png b/assets/favicon-0.png new file mode 100644 index 000000000..68830769b Binary files /dev/null and b/assets/favicon-0.png differ diff --git a/assets/favicon-5.png b/assets/favicon-5.png new file mode 100644 index 000000000..5fb1fc327 Binary files /dev/null and b/assets/favicon-5.png differ diff --git a/assets/logo-o2.svg b/assets/logo-o2.svg new file mode 100644 index 000000000..80620673b --- /dev/null +++ b/assets/logo-o2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 000000000..4ef03212e Binary files /dev/null and b/assets/logo.png differ diff --git a/config/settings.yml b/config/settings.yml index 7c561ddfb..3211d42db 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -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" diff --git a/messages/uniworx/de.msg b/messages/uniworx/de.msg index 4002461e1..68b033660 100644 --- a/messages/uniworx/de.msg +++ b/messages/uniworx/de.msg @@ -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 diff --git a/package.yaml b/package.yaml index 46af6eab8..0aadd7a3f 100644 --- a/package.yaml +++ b/package.yaml @@ -114,6 +114,7 @@ dependencies: - memcached-binary - directory-tree - lifted-base + - hsass other-extensions: - GeneralizedNewtypeDeriving diff --git a/routes b/routes index 1c1535769..aca3c5735 100644 --- a/routes +++ b/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 diff --git a/src/Foundation.hs b/src/Foundation.hs index 4938141d6..05f50b22c 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -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|

_{MsgErrorResponseNotAuthenticated}|] PermissionDenied err' -> [whamlet|

#{err'}|] BadMethod method -> [whamlet|

_{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) diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index 17bc943b9..a159489fe 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -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

^{emailWidget} |] - + defaultLayout $ -- setTitle "Uni2work Admin Testpage" $(widgetFile "adminTest") -getAdminUserR :: CryptoUUIDUser -> Handler Html -getAdminUserR uuid = do - uid <- decrypt uuid - User{..} <- runDB $ get404 uid - defaultLayout - [whamlet| -

TODO -

Admin Page for User ^{nameWidget userDisplayName userSurname} - |] - getAdminErrMsgR, postAdminErrMsgR :: Handler Html getAdminErrMsgR = postAdminErrMsgR postAdminErrMsgR = do diff --git a/src/Handler/Course.hs b/src/Handler/Course.hs index 9a55c4943..3401cf4de 100644 --- a/src/Handler/Course.hs +++ b/src/Handler/Course.hs @@ -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 diff --git a/src/Handler/Home.hs b/src/Handler/Home.hs index ae3691266..24fd36b6e 100644 --- a/src/Handler/Home.hs +++ b/src/Handler/Home.hs @@ -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") diff --git a/src/Handler/Profile.hs b/src/Handler/Profile.hs index 4551f6657..edd3c9e4f 100644 --- a/src/Handler/Profile.hs +++ b/src/Handler/Profile.hs @@ -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 diff --git a/src/Handler/Users.hs b/src/Handler/Users.hs index a2c21ec9f..4d5e6c125 100644 --- a/src/Handler/Users.hs +++ b/src/Handler/Users.hs @@ -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") diff --git a/src/Handler/Utils/DateTime.hs b/src/Handler/Utils/DateTime.hs index 1d31051fa..f67850fe2 100644 --- a/src/Handler/Utils/DateTime.hs +++ b/src/Handler/Utils/DateTime.hs @@ -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 \ No newline at end of file + | otherwise = loop (succ n) (addOneWeek t) \ No newline at end of file diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs index ae7240831..237689dd5 100644 --- a/src/Handler/Utils/Form.hs +++ b/src/Handler/Utils/Form.hs @@ -135,7 +135,6 @@ buttonForm csrf = do |]) - ------------ -- Fields -- ------------ diff --git a/src/Model/Types.hs b/src/Model/Types.hs index c72254dbd..dfedb2663 100644 --- a/src/Model/Types.hs +++ b/src/Model/Types.hs @@ -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 diff --git a/src/Settings.hs b/src/Settings.hs index f3ad5f7ac..81f98eb45 100644 --- a/src/Settings.hs +++ b/src/Settings.hs @@ -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 diff --git a/src/Settings/StaticFiles.hs b/src/Settings/StaticFiles.hs index c7bd88255..d2375a8e5 100644 --- a/src/Settings/StaticFiles.hs +++ b/src/Settings/StaticFiles.hs @@ -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 diff --git a/src/Settings/StaticFiles/Generator.hs b/src/Settings/StaticFiles/Generator.hs new file mode 100644 index 000000000..b60bcef2c --- /dev/null +++ b/src/Settings/StaticFiles/Generator.hs @@ -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) |] + } diff --git a/src/Utils.hs b/src/Utils.hs index 2990778dc..d5dee863e 100644 --- a/src/Utils.hs +++ b/src/Utils.hs @@ -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 diff --git a/src/Utils/Form.hs b/src/Utils/Form.hs index 761afbe02..17a0f362f 100644 --- a/src/Utils/Form.hs +++ b/src/Utils/Form.hs @@ -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 } diff --git a/templates/standalone/alerts.lucius b/static/css/utils/alerts.scss similarity index 99% rename from templates/standalone/alerts.lucius rename to static/css/utils/alerts.scss index bc5603220..3256eef96 100644 --- a/templates/standalone/alerts.lucius +++ b/static/css/utils/alerts.scss @@ -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; } } diff --git a/static/css/tabber.css b/static/css/utils/tabber.css similarity index 100% rename from static/css/tabber.css rename to static/css/utils/tabber.css diff --git a/static/css/flatpickr.css b/static/css/vendor/flatpickr.css similarity index 100% rename from static/css/flatpickr.css rename to static/css/vendor/flatpickr.css diff --git a/static/css/fontawesome.css b/static/css/vendor/fontawesome.css similarity index 100% rename from static/css/fontawesome.css rename to static/css/vendor/fontawesome.css diff --git a/static/favicon.ico b/static/favicon.ico index 9dd5f356d..85f9e0d7a 100644 Binary files a/static/favicon.ico and b/static/favicon.ico differ diff --git a/static/js/fetchPolyfill.js b/static/js/polyfills/fetchPolyfill.js similarity index 100% rename from static/js/fetchPolyfill.js rename to static/js/polyfills/fetchPolyfill.js diff --git a/static/js/urlPolyfill.js b/static/js/polyfills/urlPolyfill.js similarity index 100% rename from static/js/urlPolyfill.js rename to static/js/polyfills/urlPolyfill.js diff --git a/static/js/utils/alerts.js b/static/js/utils/alerts.js new file mode 100644 index 000000000..eec2fb73a --- /dev/null +++ b/static/js/utils/alerts.js @@ -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); + }; +})(); diff --git a/static/js/featureChecker.js b/static/js/utils/featureChecker.js similarity index 100% rename from static/js/featureChecker.js rename to static/js/utils/featureChecker.js diff --git a/static/js/tabber.js b/static/js/utils/tabber.js similarity index 100% rename from static/js/tabber.js rename to static/js/utils/tabber.js diff --git a/static/js/flatpickr.js b/static/js/vendor/flatpickr.js similarity index 100% rename from static/js/flatpickr.js rename to static/js/vendor/flatpickr.js diff --git a/static/js/zepto.js b/static/js/vendor/zepto.js similarity index 100% rename from static/js/zepto.js rename to static/js/vendor/zepto.js diff --git a/templates/adminUser.hamlet b/templates/adminUser.hamlet new file mode 100644 index 000000000..30d7961f5 --- /dev/null +++ b/templates/adminUser.hamlet @@ -0,0 +1,3 @@ + + ^{formWidget} + ^{submitButtonView} diff --git a/templates/authpreds.hamlet b/templates/authpreds.hamlet index d7430fbae..f9ba8cb05 100644 --- a/templates/authpreds.hamlet +++ b/templates/authpreds.hamlet @@ -1,3 +1,4 @@ +_{MsgAuthPredsInfo} $maybe referer <- mReferer diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet index bf936affd..87870650b 100644 --- a/templates/default-layout.hamlet +++ b/templates/default-layout.hamlet @@ -30,12 +30,7 @@ $if not isModal ^{widget} -
- $forall (status, msg) <- mmsgs - $with status2 <- bool status "info" (status == "") -
-
- #{msg} + ^{alerts} $if not isModal diff --git a/templates/default-layout.lucius b/templates/default-layout.lucius index faf42548f..c90f17efb 100644 --- a/templates/default-layout.lucius +++ b/templates/default-layout.lucius @@ -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; } diff --git a/templates/infoLecturer.hamlet b/templates/infoLecturer.hamlet new file mode 100644 index 000000000..12ff31f87 --- /dev/null +++ b/templates/infoLecturer.hamlet @@ -0,0 +1,113 @@ + +UniWorX erfahrene Veranstalter finden +hier die wichtigsten Neuerungen. +
+

Veranstaltungen + +
+
Kurskürzel +
+ Alle Veranstaltungen müssen ein Kürzel zur Identifikation besitzen, + z.B. EiP, ProMo, SysPrak, etc. +
+ Das Kürzel muss innerhalb Institut und Semester eindeutig sein. + +
Kurse klonen +
+ Veranstalter können alle Kurse Ihres Instituts für das aktuelle Semesters klonen. +
+ Dabei werden vor allem Kurskürzel und die Kursbeschreibung übernommen; + nicht jedoch Übungsblätter, Klausuren oder Anmeldungen. +
+ Die Kursbeschreibung kann in Html verfasst werden und + sollte die Modulbeschreibung enthalten! + +
Passwort +
Die Anmeldung zum Kurs kann durch ein Passwort geschützt werden. + +
Materialzugriff +
+ Der Zugriff auf Übungsblätter, Folien und andere Materialien + kann von der Anmeldung zum Kurs abhängig gemacht werden. + +
Teilnehmer +
+ Anzeige und Benachrichtigung angemeldeter + Teilnehmer ist leider noch nicht fertig implementiert. + Voraussichtlich noch vor Start des Sommersemesters 2019 verfügbar. + +
+

Übungsbetrieb + +
+
Korrektoren +
+ 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. + +
Verteilung +
+ 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. + +
Dateien +
+ 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. + +
Lösungshinweise +
+ Zusätzlich zu Aufgabe und Lösung können Hinweise ab einem + Datum vor Abgabfrist freigeschaltet werden, + z.B. Lösungen zu Präsenzaufgaben. + +
Sichtbarkeit +
+ Übungsblätter können bis zu einem Datum vor den Teilnehmern versteckt werden. + +
Zeitstempel +
+ Alle Dateien eines Übungsblattes sind mit einem + für Teilnehmer sichtbaren Zeitstempel versehen. +
+ Eine visuelle Hervorhebung geänderter/neuer Dateien + und entsprechende Benachrichtigungen sind geplant, + aber noch nicht verfügbar. + +
Übungsgruppen +
+ Eine Anmeldung zu Übungsgruppen wie bisher + ist leider noch nicht fertig implementiert. + +
Papierabgaben +
+ Abgaben in anderer Form (z.B. Papierabgaben) + können mit Hilfe von Tokens verwaltet werden. + Korrekturen können elektronisch zurückgegeben werden. + +
+

Klausuren + Das Verwalten von Klausuren und Notenmeldungen + ist leider noch nicht fertig implementiert. + +
+

Sonstiges +
+ +
Wartung +
+ 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. \ No newline at end of file diff --git a/templates/profileData.hamlet b/templates/profileData.hamlet index 5acedae8e..175f98f3d 100644 --- a/templates/profileData.hamlet +++ b/templates/profileData.hamlet @@ -117,9 +117,8 @@

Hinweise:
  • - 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.
  • Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrektur von Übungen, Übungsgruppenleiterschaft, Raumbuchungen, etc.
  • diff --git a/templates/standalone/alerts.hamlet b/templates/standalone/alerts.hamlet deleted file mode 100644 index 163f415b4..000000000 --- a/templates/standalone/alerts.hamlet +++ /dev/null @@ -1 +0,0 @@ - diff --git a/templates/standalone/alerts.julius b/templates/standalone/alerts.julius deleted file mode 100644 index c8c04dc14..000000000 --- a/templates/standalone/alerts.julius +++ /dev/null @@ -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 })) -}); diff --git a/templates/standalone/inputs.lucius b/templates/standalone/inputs.lucius index 84214f862..f30155892 100644 --- a/templates/standalone/inputs.lucius +++ b/templates/standalone/inputs.lucius @@ -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: ''; diff --git a/templates/standalone/modal.lucius b/templates/standalone/modal.lucius index 722f38a3a..0718a4367 100644 --- a/templates/standalone/modal.lucius +++ b/templates/standalone/modal.lucius @@ -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; diff --git a/templates/table/layout.julius b/templates/table/layout.julius index 43a6595a3..296f03721 100644 --- a/templates/table/layout.julius +++ b/templates/table/layout.julius @@ -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); } }); diff --git a/templates/table/layout.lucius b/templates/table/layout.lucius index c35fcecde..c03851b27 100644 --- a/templates/table/layout.lucius +++ b/templates/table/layout.lucius @@ -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; } } diff --git a/templates/widgets/alerts/alerts.hamlet b/templates/widgets/alerts/alerts.hamlet new file mode 100644 index 000000000..aa7169ca1 --- /dev/null +++ b/templates/widgets/alerts/alerts.hamlet @@ -0,0 +1,9 @@ +
    +
    + $forall (status, msg) <- mmsgs + $with status2 <- bool status "info" (status == "") +
    +
    +
    +
    + #{msg} diff --git a/templates/widgets/alerts/alerts.julius b/templates/widgets/alerts/alerts.julius new file mode 100644 index 000000000..14e679f61 --- /dev/null +++ b/templates/widgets/alerts/alerts.julius @@ -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 })) +}); diff --git a/templates/widgets/asidenav.lucius b/templates/widgets/asidenav.lucius index 4086d946e..9123c407d 100644 --- a/templates/widgets/asidenav.lucius +++ b/templates/widgets/asidenav.lucius @@ -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; diff --git a/templates/widgets/navbar.lucius b/templates/widgets/navbar.lucius index 7cba47a78..335c59d03 100644 --- a/templates/widgets/navbar.lucius +++ b/templates/widgets/navbar.lucius @@ -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); } diff --git a/templates/widgets/user-rights-form.hamlet b/templates/widgets/user-rights-form.hamlet new file mode 100644 index 000000000..a77f054fe --- /dev/null +++ b/templates/widgets/user-rights-form.hamlet @@ -0,0 +1,14 @@ +$newline never +#{csrf} +
    + + + +
    + $# empty cell + _{MsgAdminFor} + _{MsgLecturerFor} + $forall (Entity _ (School name _), (_,cbAdmin), (_,cbLecturer)) <- boxRights +
    #{name} + ^{fvInput cbAdmin} + ^{fvInput cbLecturer} \ No newline at end of file