Merge and by accident info page

This commit is contained in:
SJost 2019-02-21 11:35:04 +01:00
commit 10237c4031
105 changed files with 1622 additions and 865 deletions

View File

@ -67,7 +67,7 @@ TermSchoolCourseListTitle tid@TermId school@SchoolName: Kurse #{display tid} fü
CourseNewHeading: Neuen Kurs anlegen
CourseEditHeading tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} editieren
CourseEditTitle: Kurs editieren/anlegen
CourseMembers: Teilnehmer
CourseMember: Teilnehmer
CourseMembersCount num@Int64: #{display num}
CourseMembersCountLimited num@Int64 max@Int64: #{display num}/#{display max}
CourseName: Name
@ -88,6 +88,7 @@ CourseFilterSearch: Volltext-Suche
CourseFilterRegistered: Registriert
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
CourseDeleted: Kurs gelöscht
CourseUserNote: Notiz
NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht.
NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht.
@ -235,6 +236,7 @@ ProfileHeading: Benutzereinstellungen
ProfileFor: Benutzereinstellungen für
ProfileDataHeading: Gespeicherte Benutzerdaten
ImpressumHeading: Impressum
DataProtHeading: Datenschutzerklärung
SystemMessageHeading: Uni2work Statusmeldung
SystemMessageListHeading: Uni2work Statusmeldungen
@ -340,6 +342,7 @@ AccessRightsFor: Berechtigungen für
AdminFor: Administrator
LecturerFor: Dozent
LecturersFor: Dozenten
ForSchools n@Int: für #{pluralDE n "Institut" "Institute"}
UserListTitle: Komprehensive Benutzerliste
AccessRightsSaved: Berechtigungsänderungen wurden gespeichert.
@ -420,11 +423,17 @@ MailSubmissionsUnassignedIntro n@Int courseName@Text termDesc@Text sheetName@She
MailSubjectSheetSoonInactive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} kann nur noch kurze Zeit abgegeben werden
MailSheetSoonInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) endet in Kürze.
MailSubjectSheetInactive csh@CourseShorthand sheetName@SheetName: Abgabfrist für #{sheetName} in #{csh} abgelaufen
MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Die Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet.
MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName n@Int num@Int64: Die Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet. Es gab #{noneOneMoreDE n "Keine Abgaben" "Nur eine Abgabe von " (display n <> " Abgaben von ")}#{noneOneMoreDE num "" "einem Teilnehmer" (display num <> " Teilnehmern")}.
MailSubjectCorrectionsAssigned csh@CourseShorthand sheetName@SheetName: Ihnen wurden Korrekturen zu #{sheetName} in #{csh} zugeteilt
MailCorrectionsAssignedIntro courseName@Text termDesc@Text sheetName@SheetName n@Int: #{display n} #{pluralDE n "Abgabe wurde" "Abgaben wurden"} Ihnen zur Korrektur für #{sheetName} im Kurs #{courseName} (#{termDesc}) zugeteilt.
MailSubjectUserRightsUpdate name@Text: Berechtigungen für #{name} aktualisiert
MailUserRightsIntro name@Text email@UserEmail: #{name} <#{email}> hat folgende Uni2work Berechtigungen:
MailNoLecturerRights: Sie haben derzeit keine Dozenten-Rechte.
MailLecturerRights n@Int: Als Dozent dürfen Sie Veranstaltungen innerhalb #{pluralDE n "Ihres Instituts" "Ihrer Institute"} anlegen.
MailEditNotifications: Benachrichtigungen ein-/ausschalten
MailSubjectSupport: Supportanfrage
@ -473,6 +482,7 @@ NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr
NotificationTriggerSheetInactive: Abgabefrist eines meiner Übungsblätter ist abgelaufen
NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt
NotificationTriggerCorrectionsNotDistributed: Abgaben eines meiner Übungsblätter konnten keinem Korrektur zugeteilt werden
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
CorrCreate: Abgaben erstellen
UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}"
@ -571,6 +581,7 @@ InvalidRoute: Konnte URL nicht interpretieren
MenuHome: Aktuell
MenuImpressum: Impressum
MenuDataProt: Datenschutz
MenuVersion: Versionsgeschichte
MenuHelp: Hilfe
MenuProfile: Anpassen

View File

@ -1,7 +1,7 @@
School json
name (CI Text)
shorthand (CI Text)
shorthand (CI Text) -- SchoolKey :: SchoolShorthand -> SchoolId
UniqueSchool name
UniqueSchoolShorthand shorthand -- required for Normalisation of CI Text
Primary shorthand -- newtype Key School = SchoolKey { unSchoolKey :: SchoolShorthand }
Primary shorthand -- newtype Key School = SchoolKey { unSchoolKey :: SchoolShorthand }
deriving Eq Show Generic

37
routes
View File

@ -10,22 +10,23 @@
-- Admins always have access to entities within their assigned schools.
--
-- Access Tags:
-- !free -- free for all
-- !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)
-- !owner -- part of the group of owners of this submission
-- !capacity -- course this route is associated with has at least one unit of participant capacity
-- !empty -- course this route is associated with has no participants whatsoever
-- !free -- free for all
-- !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)
-- !owner -- part of the group of owners of this submission
-- !capacity -- course this route is associated with has at least one unit of participant capacity
-- !empty -- course this route is associated with has no participants whatsoever
--
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
-- !time -- access depends on time somehow
-- !read -- only if it is read-only access (i.e. GET but not POST)
-- !write -- only if it is write access (i.e. POST only, included for completeness)
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
-- !time -- access depends on time somehow
-- !read -- only if it is read-only access (i.e. GET but not POST)
-- !write -- only if it is write access (i.e. POST only, included for completeness)
--
-- !deprecated -- like free, but logs and gives a warning; entirely disabled in production
-- !development -- like free, but only for development builds
-- !no-escalation --
-- !deprecated -- like free, but logs and gives a warning; entirely disabled in production
-- !development -- like free, but only for development builds
/static StaticR EmbeddedStatic appStatic !free
/auth AuthR Auth getAuth !free
@ -39,9 +40,13 @@
/users/#CryptoUUIDUser/hijack AdminHijackUserR POST !adminANDno-escalation
/admin/test AdminTestR GET POST
/admin/errMsg AdminErrMsgR GET POST
/impressum ImpressumR GET !free
/version VersionR GET !free
/info InfoR GET !free
/impressum ImpressumR GET !free
/info/data DataProtR GET !free
/version VersionR GET !free
/help HelpR GET POST !free
/help/lecturer InfoLecturerR GET !lecturer

View File

@ -7,7 +7,7 @@ import Import.NoFoundation
import Database.Persist.Sql (SqlBackendCanRead)
import Utils.Form
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
@ -54,4 +54,4 @@ dummyLogin = AuthPlugin{..}
apDispatch _ _ = notFound
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm
$(widgetFile "widgets/dummy-login-form")
$(widgetFile "widgets/dummy-login-form/dummy-login-form")

View File

@ -36,7 +36,7 @@ data CampusMessage = MsgCampusIdentNote
| MsgCampusSubmit
| MsgCampusInvalidCredentials
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
findUser LdapConf{..} ldap campusIdent = Ldap.search ldap ldapBase userSearchSettings userFilter
@ -48,7 +48,7 @@ findUser LdapConf{..} ldap campusIdent = Ldap.search ldap ldapBase userSearchSet
, Ldap.time ldapSearchTimeout
, Ldap.derefAliases Ldap.DerefAlways
]
userPrincipalName :: Ldap.Attr
userPrincipalName = Ldap.Attr "userPrincipalName"
@ -105,7 +105,7 @@ campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
apDispatch _ _ = notFound
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard campusForm
$(widgetFile "widgets/campus-login-form")
$(widgetFile "widgets/campus-login/campus-login-form")
data CampusUserException = CampusUserLdapError LdapPoolError
| CampusUserHostNotResolved String

View File

@ -35,7 +35,7 @@ hashForm = HashLogin
<*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing
<* submitButton
hashLogin :: ( YesodAuth site
, YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site)
@ -90,5 +90,5 @@ hashLogin pwHashAlgo = AuthPlugin{..}
apDispatch _ _ = notFound
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm
$(widgetFile "widgets/hash-login-form")
$(widgetFile "widgets/hash-login-form/hash-login-form")

View File

@ -0,0 +1,45 @@
module Database.Esqueleto.Utils where
import ClassyPrelude.Yesod hiding (isInfixOf, (||.))
import Data.Foldable as F
import Database.Esqueleto as E
{-|
Description : Convenience for using @Esqueleto@,
intended to be imported qualified
just like Esqueleto
-}
-- ezero = E.val (0 :: Int64)
-- | Often needed with this concrete type
true :: E.SqlExpr (E.Value Bool)
true = E.val True
-- | Often needed with this concrete type
false :: E.SqlExpr (E.Value Bool)
false = E.val False
-- | Check if the first string is contained in the text derived from the second argument
isInfixOf :: (E.Esqueleto query expr backend, E.SqlString s2) =>
Text -> expr (E.Value s2) -> expr (E.Value Bool)
isInfixOf needle strExpr = E.castString strExpr `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)
hasInfix :: (E.Esqueleto query expr backend, E.SqlString s2) =>
expr (E.Value s2) -> Text -> expr (E.Value Bool)
hasInfix = flip isInfixOf
-- | Given a test and a set of values, check whether anyone succeeds the test
-- WARNING: SQL leaves it explicitely unspecified whether || is short curcuited (i.e. lazily evaluated)
any :: Foldable f =>
(a -> SqlExpr (E.Value Bool)) -> f a -> SqlExpr (E.Value Bool)
any test = F.foldr (\needle acc -> acc ||. test needle) false
-- | Given a test and a set of values, check whether all succeeds the test
-- WARNING: SQL leaves it explicitely unspecified whether && is short curcuited (i.e. lazily evaluated)
all :: Foldable f =>
(a -> SqlExpr (E.Value Bool)) -> f a -> SqlExpr (E.Value Bool)
all test = F.foldr (\needle acc -> acc &&. test needle) true

View File

@ -157,6 +157,19 @@ pluralDE num singularForm pluralForm
| num == 1 = singularForm
| otherwise = pluralForm
noneOneMoreDE :: (Eq a, Num a)
=> a -- ^ Count
-> Text -- ^ None
-> Text -- ^ Singular
-> Text -- ^ Plural
-> Text
noneOneMoreDE num noneText singularForm pluralForm
| num == 0 = noneText
| num == 1 = singularForm
| otherwise = pluralForm
-- Messages creates type UniWorXMessage and RenderMessage UniWorX instance
mkMessage "UniWorX" "messages/uniworx" "de"
mkMessageVariant "UniWorX" "Campus" "messages/campus" "de"
@ -969,19 +982,19 @@ siteLayout' headingOverride widget = do
-- you to use normal widget features in default-layout.
navbar :: Widget
navbar = $(widgetFile "widgets/navbar")
navbar = $(widgetFile "widgets/navbar/navbar")
asidenav :: Widget
asidenav = $(widgetFile "widgets/asidenav")
asidenav = $(widgetFile "widgets/asidenav/asidenav")
footer :: Widget
footer = $(widgetFile "widgets/footer")
footer = $(widgetFile "widgets/footer/footer")
alerts :: Widget
alerts = $(widgetFile "widgets/alerts/alerts")
contentHeadline :: Maybe Widget
contentHeadline = headingOverride <|> (pageHeading =<< mcurrentRoute)
breadcrumbsWgt :: Widget
breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs")
breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs/breadcrumbs")
pageaction :: Widget
pageaction = $(widgetFile "widgets/pageaction")
pageaction = $(widgetFile "widgets/pageaction/pageaction")
-- functions to determine if there are page-actions (primary or secondary)
hasPageActions, hasSecondaryPageActions, hasPrimaryPageActions :: Bool
hasPageActions = hasPrimaryPageActions || hasSecondaryPageActions
@ -989,25 +1002,35 @@ siteLayout' headingOverride widget = do
hasPrimaryPageActions = any (is _PageActionPrime) $ toListOf (traverse . _1 . _menuItemType) menuTypes
pc <- widgetToPageContent $ do
addScript $ StaticR js_vendor_zepto_js
-- 3rd party
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
addScript $ StaticR js_vendor_zepto_js
addStylesheet $ StaticR css_vendor_flatpickr_css
addStylesheet $ StaticR css_vendor_fontawesome_css
-- fonts
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_utils_tabber_css
-- polyfills
addScript $ StaticR js_polyfills_fetchPolyfill_js
addScript $ StaticR js_polyfills_urlPolyfill_js
-- JavaScript utils
addScript $ StaticR js_utils_alerts_js
addScript $ StaticR js_utils_asidenav_js
addScript $ StaticR js_utils_asyncTable_js
addScript $ StaticR js_utils_form_js
addScript $ StaticR js_utils_inputs_js
addScript $ StaticR js_utils_setup_js
addScript $ StaticR js_utils_showHide_js
addScript $ StaticR js_utils_tabber_js
addStylesheet $ StaticR css_utils_alerts_scss
addStylesheet $ StaticR css_utils_asidenav_scss
addStylesheet $ StaticR css_utils_form_scss
addStylesheet $ StaticR css_utils_inputs_scss
addStylesheet $ StaticR css_utils_showHide_scss
addStylesheet $ StaticR css_utils_tabber_scss
addStylesheet $ StaticR css_utils_tooltip_scss
-- widgets
$(widgetFile "default-layout")
$(widgetFile "standalone/modal")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/inputs")
$(widgetFile "standalone/tooltip")
$(widgetFile "standalone/tabber")
$(widgetFile "standalone/datepicker")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
@ -1038,13 +1061,18 @@ 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 HelpR = return ("Hilfe" , 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 InfoR = return ("Information" , Nothing)
breadcrumb ImpressumR = return ("Impressum" , Just InfoR)
breadcrumb DataProtR = return ("Datenschutz" , Just InfoR)
breadcrumb VersionR = return ("Impressum" , Just InfoR)
breadcrumb HelpR = return ("Hilfe" , Just HomeR)
breadcrumb InfoLecturerR = return ("Veranstalter" , Just HelpR)
breadcrumb ProfileR = return ("User" , Just HomeR)
@ -1116,10 +1144,18 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
, menuItemModal = False
, menuItemAccessCallback' = return True
}
, return MenuItem
{ menuItemType = Footer
, menuItemLabel = MsgMenuDataProt
, menuItemIcon = Just "shield"
, menuItemRoute = SomeRoute DataProtR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
, return MenuItem
{ menuItemType = Footer
, menuItemLabel = MsgMenuImpressum
, menuItemIcon = Just "book"
, menuItemIcon = Just "file-signature"
, menuItemRoute = SomeRoute ImpressumR
, menuItemModal = False
, menuItemAccessCallback' = return True
@ -1161,17 +1197,17 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
}
, return MenuItem
{ menuItemType = NavbarAside
, menuItemLabel = MsgMenuCourseList
, menuItemIcon = Just "graduation-cap"
, menuItemRoute = SomeRoute CourseListR
, menuItemLabel = MsgMenuTermShow
, 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
, menuItemLabel = MsgMenuTermShow
, 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
, menuItemLabel = MsgMenuCourseList
, menuItemIcon = Just "graduation-cap"
, menuItemRoute = SomeRoute CourseListR
, menuItemModal = False
, menuItemAccessCallback' = return True
}

View File

@ -162,7 +162,7 @@ colRating = sortable (Just "rating") (i18nCell MsgRating) $ \DBRow{ dbrOutput=(E
cid <- encrypt subId
return $ CSubmissionR tid ssh csh sheetName cid CorrectionR
in mconcat
[ anchorCellM mkRoute $(widgetFile "widgets/rating")
[ anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
, writerCell $ do
let
summary :: SheetTypeSummary

View File

@ -1,3 +1,5 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Course where
import Import
@ -89,8 +91,8 @@ colRegTo = sortable (Just "register-to") (i18nCell MsgRegisterTo)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
maybe mempty timeCell courseRegisterTo
colParticipants :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colParticipants = sortable (Just "participants") (i18nCell MsgCourseMembers)
colMembers :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colMembers = sortable (Just "members") (i18nCell MsgCourseMember)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of
Nothing -> MsgCourseMembersCount currentParticipants
Just limit -> MsgCourseMembersCountLimited currentParticipants limit
@ -137,7 +139,7 @@ makeCourseTable whereClause colChoices psValidator = do
, ( "schoolshort", SortColumn $ \(_course `E.InnerJoin` school) -> school E.^. SchoolShorthand)
, ( "register-from", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterFrom)
, ( "register-to", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterTo)
, ( "participants", SortColumn course2Participants )
, ( "members", SortColumn course2Participants )
, ( "registered", SortColumn $ course2Registered muid)
]
, dbtFilter = Map.fromList -- OverloadedLists does not work with the templates here
@ -221,7 +223,7 @@ getTermSchoolCourseListR tid ssh = do
, colCShortDescr
, colRegFrom
, colRegTo
, colParticipants
, colMembers
, maybe mempty (const colRegistered) muid
]
whereClause (course, _, _) =
@ -245,7 +247,7 @@ getTermCourseListR tid = do
, colSchoolShort
, colRegFrom
, colRegTo
, colParticipants
, colMembers
, maybe mempty (const colRegistered) muid
]
whereClause (course, _, _) = course E.^. CourseTerm E.==. E.val tid
@ -295,7 +297,7 @@ registerForm registered msecret extra = do
(Just _) | not registered -> bimap Just Just <$> mreq textField (fslpI MsgCourseSecret "Code") Nothing
_ -> return (Nothing,Nothing)
(btnRes, btnView) <- mreq (buttonField $ bool BtnRegister BtnDeregister registered) "buttonField ignores settings anyway" Nothing
let widget = $(widgetFile "widgets/registerForm")
let widget = $(widgetFile "widgets/register-form/register-form")
let msecretRes | Just res <- msecretRes' = Just <$> res
| otherwise = FormSuccess Nothing
return (btnRes *> ((==msecret) <$> msecretRes), widget) -- checks that correct button was pressed, and ignores result of btnRes
@ -617,8 +619,81 @@ validateCourse CourseForm{..} =
-- CourseUserTable
type UserTableExpr = (E.SqlExpr (Entity User) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant)) `E.LeftOuterJoin` E.SqlExpr (Entity CourseUserNote)
type UserTableWhere = UserTableExpr -> E.SqlExpr (E.Value Bool)
type UserTableData = DBRow (Entity User, Entity CourseParticipant, Maybe (Key CourseUserNote))
userTableQuery :: UserTableWhere -> (UserTableExpr -> v) -> UserTableExpr -> E.SqlQuery v
userTableQuery whereClause returnStatement t@((user `E.InnerJoin` participant) `E.LeftOuterJoin` note) = do
E.on $ participant E.^. CourseParticipantUser E.==. note E.^. CourseUserNoteUser
E.on $ participant E.^. CourseParticipantUser E.==. user E.^. UserId
E.where_ $ whereClause t
return $ returnStatement t
instance HasUser UserTableData where
hasUser = _entityVal
instance HasEntity UserTableData User where
hasEntity = _dbrOutput . _1
-- -- there can be only one -- FunctionalDependency violation
-- instance HasEntity UserTableData CourseParticipant where
-- hasEntity = _dbrOutput . _2
courseIs :: CourseId -> UserTableWhere
courseIs cid ((_user `E.InnerJoin` participant) `E.LeftOuterJoin` _note) = participant E.^. CourseParticipantCourse E.==. E.val cid
-- TODO: delete commented function
-- colUserParticipant' :: IsDBTable m a => Colonnade _ UserTableData (DBCell m a)
-- colUserParticipant' = sortable (Just "participant") (i18nCell MsgCourseMember)
-- $ \DBRow { dbrOutput=(Entity _ user,_,_) } -> userCell (userDisplayName user) (userSurname user)
colUserParticipant :: IsDBTable m a => Colonnade _ UserTableData (DBCell m a)
colUserParticipant = sortable (Just "participant") (i18nCell MsgCourseMember) cellHasUser
colUserParticipantLink :: IsDBTable m a => TermId -> SchoolId -> CourseShorthand -> Colonnade _ UserTableData (DBCell m a)
colUserParticipantLink tid ssh csh = sortable (Just "participant") (i18nCell MsgCourseMember) (cellHasUserLink courseLink)
where
courseLink = CourseR tid ssh csh . CUserR
colUserMatriclenr :: IsDBTable m a => Colonnade _ UserTableData (DBCell m a)
colUserMatriclenr = sortable (Just "matriclenumber") (i18nCell MsgMatrikelNr) cellHasMatrikelnummer
colUserComment :: IsDBTable m a => TermId -> SchoolId -> CourseShorthand -> Colonnade _ UserTableData (DBCell m a)
colUserComment tid ssh csh =
sortable (Just "course-user-note") (i18nCell MsgCourseUserNote)
$ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey) } ->
maybeEmpty mbNoteKey $ const $
anchorCellM (courseLink <$> encrypt uid) (commentWidget True)
where
courseLink = CourseR tid ssh csh . CUserR
makeUserTable :: UserTableWhere -> _ -> _ -> DB Widget
makeUserTable _whereClause _colChoices _psValidator =
-- do
-- dbTable psValidator DBTable
-- { userTableQUery whereClause
-- ,
return [whamlet| Course user table not yet implemented |]
getCUsersR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCUsersR = error "CUsersR: Not implemented"
getCUsersR tid ssh csh = do
Entity _cid course <- runDB $ getBy404 $ TermSchoolCourseShort tid ssh csh
let heading = [whamlet|_{MsgCourseMember} #{courseName course} #{display tid}|]
-- whereClause = courseIs cid
-- colChoices = [colUserParticipant,colUserMatriclenr,colUserComment tid ssh csh]
-- psValidator = def
-- tableWidget <- runDB $ makeUserTable whereClause colChoices psValidator
siteLayout heading $ do
setTitle [shamlet| #{toPathPiece tid} - #{csh}|]
[whamlet|
User table not yet implemented
$# ^{tableWidget}
|]
getCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html

View File

@ -183,16 +183,31 @@ homeUser uid = do
$(widgetFile "homeUser")
-- (widgetFile "dsgvDisclaimer")
-- | Versionsgeschichte
getVersionR :: Handler TypedContent
getVersionR = getInfoR -- TODO
getImpressumR :: Handler TypedContent
getImpressumR = getInfoR -- TODO
-- | Impressum
getImpressumR :: Handler Html
getImpressumR = -- do
siteLayoutMsg' MsgMenuImpressum $ do
setTitleI MsgImpressumHeading
$(widgetFile "impressum")
-- | Hinweise zu Datenschutz und Aufbewahrungspflichten
getDataProtR :: Handler Html
getDataProtR = -- do
siteLayoutMsg' MsgMenuDataProt $ do
setTitleI MsgDataProtHeading
$(widgetFile "data-protection-de")
-- | Allgemeine Informationen
getInfoR :: Handler TypedContent
getInfoR = selectRep $ do
provideRep . defaultLayout $ do
let infoHeading = [whamlet|Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>|]
provideRep . siteLayout infoHeading $ do
let features = $(widgetFile "featureList")
gitInfo :: Text
gitInfo = $gitDescribe <> " (" <> $gitCommitDate <> ")"

View File

@ -251,7 +251,7 @@ getProfileDataR = do
-- Delete Button
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form ButtonDelete)
defaultLayout $ do
let delWdgt = $(widgetFile "widgets/data-delete")
let delWdgt = $(widgetFile "widgets/data-delete/data-delete")
$(widgetFile "profileData")
$(widgetFile "dsgvDisclaimer")

View File

@ -205,7 +205,7 @@ getSheetListR tid ssh csh = do
mkRoute = do
cid' <- mkCid
return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR
acell = anchorCellM mkRoute $(widgetFile "widgets/rating")
acell = anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
in cellTell acell $ stats submissionRatingPoints
, sortable Nothing -- (Just "percent")

View File

@ -1,6 +1,8 @@
module Handler.Users where
import Import
import Jobs
-- import Data.Text
import Handler.Utils
@ -12,15 +14,21 @@ import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
hijackUserForm :: CryptoUUIDUser -> Form ()
hijackUserForm cID csrf = do
(uidResult, uidView) <- mforced hiddenField "" (cID :: CryptoUUIDUser)
(btnResult, btnView) <- mreq (buttonField BtnHijack) "" Nothing
return (() <$ uidResult <* btnResult, mconcat [toWidget csrf, fvInput uidView, fvInput btnView])
-- In case of refactoring, use this:
-- instance HasEntity (DBRow (Entity User)) User where
-- hasEntity = _dbrOutput
-- instance HasUser (DBRow (Entity USer)) where
-- hasUser = _entityVal
getUsersR :: Handler Html
getUsersR = do
let
@ -71,28 +79,60 @@ 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)
, dbtColonnade
, dbtProj = return
, dbtSorting = Map.fromList
[ ( "name"
, SortColumn $ \user -> user E.^. UserSurname
)
, ( "display-name"
, SortColumn $ \user -> user E.^. UserDisplayName
)
, ( "matriculation"
, SortColumn $ \user -> user E.^. UserMatrikelnummer
)
]
, dbtFilter = mempty
, dbtFilterUI = mempty
, dbtStyle = def
, dbtParams = def
, dbtIdent = "users" :: Text
}
((), userList) <- runDB $ do
schoolOptions <- map (CI.original . schoolName . entityVal &&& CI.original . unSchoolKey . entityKey)
<$> selectList [] [Asc SchoolName]
dbTable psValidator DBTable
{ dbtSQLQuery = return :: E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (Entity User))
, dbtRowKey = (E.^. UserId)
, dbtColonnade
, dbtProj = return
, dbtSorting = Map.fromList
[ ( "name"
, SortColumn $ \user -> user E.^. UserSurname
)
, ( "display-name"
, SortColumn $ \user -> user E.^. UserDisplayName
)
, ( "matriculation"
, SortColumn $ \user -> user E.^. UserMatrikelnummer
)
]
, dbtFilter = Map.fromList -- OverloadedLists does not work with the templates
[ ( "user-search", FilterColumn $ \user criterion ->
if Set.null criterion then E.true else -- TODO: why is this condition not needed?
-- Set.foldr (\needle acc -> acc E.||. (user E.^. UserDisplayName) `E.hasInfix` needle) eFalse (criterion :: Set.Set Text)
E.any (user E.^. UserDisplayName `E.hasInfix`) criterion
)
, ( "matriculation", FilterColumn $ \user (criterion :: Set.Set Text) -> if
| Set.null criterion -> E.true -- TODO: why can this be eFalse and work still?
| otherwise -> E.any (user E.^. UserMatrikelnummer `E.hasInfix`) criterion
)
, ( "school", FilterColumn $ \user criterion -> if
| Set.null criterion -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> let schools = E.valList (Set.toList criterion) in
E.exists ( E.from $ \ulectr -> do
E.where_ $ ulectr E.^. UserLecturerUser E.==. user E.^. UserId
E.where_ $ ulectr E.^. UserLecturerSchool `E.in_` schools
) E.||.
E.exists ( E.from $ \uadmin -> do
E.where_ $ uadmin E.^. UserAdminUser E.==. user E.^. UserId
E.where_ $ uadmin E.^. UserAdminSchool `E.in_` schools
)
)
]
, dbtFilterUI = \mPrev -> mconcat
[ prismAForm (singletonFilter "user-search") mPrev $ aopt (searchField True) (fslI MsgName)
-- , prismAForm (singletonFilter "matriculation" ) mPrev $ aopt (searchField False) (fslI MsgMatrikelNr)
, prismAForm (singletonFilter "matriculation" ) mPrev $ aopt matriculationField (fslI MsgMatrikelNr)
, prismAForm (singletonFilter "school" ) mPrev $ aopt (lift `hoistField` selectFieldList schoolOptions) (fslI MsgCourseSchool)
]
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
, dbtParams = def
, dbtIdent = "users" :: Text
}
defaultLayout $ do
setTitleI MsgUserListTitle
@ -116,7 +156,8 @@ postAdminUserR uuid = do
adminId <- requireAuthId
uid <- decrypt uuid
let fromSchoolList = Set.fromList . map (userAdminSchool . entityVal)
(User{..}, fromSchoolList -> adminSchools, userRights) <- runDB $ (,,)
let unValueRights (school, E.Value isAdmin, E.Value isLecturer) = (school,isAdmin,isLecturer)
(User{..}, fromSchoolList -> adminSchools, fmap unValueRights -> userRights) <- runDB $ (,,)
<$> get404 uid
<*> selectList [UserAdminUser ==. adminId] []
<*> E.select ( E.from $ \school -> do
@ -132,7 +173,7 @@ postAdminUserR uuid = do
-- 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) ->
boxRights <- forM userRights $ \(school@(Entity sid _), isAdmin, isLecturer) ->
if Set.member sid adminSchools
then do
cbAdmin <- mreq checkBoxField "" (Just isAdmin)
@ -144,7 +185,7 @@ postAdminUserR uuid = do
return (school, cbAdmin, cbLecturer)
let result = forM boxRights $ \(Entity sid _, (resAdmin,_), (resLecturer, _)) ->
(,,) <$> pure sid <*> resAdmin <*> resLecturer
return (result,$(widgetFile "widgets/user-rights-form"))
return (result,$(widgetFile "widgets/user-rights-form/user-rights-form"))
let userRightsAction changes = do
void . runDB $
forM changes $ \(sid, userAdmin, userLecturer) ->
@ -158,6 +199,7 @@ postAdminUserR uuid = do
then void . insertUnique $ UserLecturer uid sid
else deleteBy $ UniqueSchoolLecturer uid sid
-- Note: deleteWhere would not work well here since we filter by adminSchools
queueJob' . JobQueueNotification $ NotificationUserRightsUpdate uid (over _1 (schoolShorthand . entityVal) <$> userRights) -- original rights to check for difference
addMessageI Info MsgAccessRightsSaved
((result, formWidget),formEnctype) <- runFormPost userRightsForm
formResult result userRightsAction

View File

@ -74,3 +74,8 @@ visibleWidget :: Bool -> Widget
-- ^ @visibleWidget False@ is an icon that denotes that something™ is not visible
visibleWidget True = mempty
visibleWidget False = [whamlet|<i .fas .fa-eye-slash>|]
commentWidget :: Bool -> Widget
-- ^ @commentWidget True@ is an icon that denotes that something™ has a comment
commentWidget True = [whamlet|<i .fas .fa-comment-alt>|]
commentWidget False = mempty

View File

@ -0,0 +1,31 @@
module Handler.Utils.Database
( getSchoolsOf
, makeSchoolDictionaryDB, makeSchoolDictionary
) where
import Import
import Data.Map as Map
-- import Data.CaseInsensitive (CI)
-- import qualified Data.CaseInsensitive as CI
import qualified Database.Esqueleto as E
makeSchoolDictionaryDB :: DB (Map.Map SchoolId SchoolName)
makeSchoolDictionaryDB = makeSchoolDictionary <$> selectList [] [Asc SchoolShorthand]
makeSchoolDictionary :: [Entity School] -> Map.Map SchoolId SchoolName
makeSchoolDictionary schools = Map.fromDistinctAscList [ (ssh,schoolName) | Entity ssh School{schoolName} <- schools ]
-- getSchoolsOf :: ( BaseBackend backend ~ SqlBackend
-- , PersistEntityBackend val ~ SqlBackend
-- , PersistUniqueRead backend, PersistQueryRead backend
-- , IsPersistBackend backend, PersistEntity val, MonadIO m) =>
-- UserId -> EntityField val SchoolId -> EntityField val UserId -> ReaderT backend m [E.Value SchoolName]
getSchoolsOf :: (PersistEntity val, PersistEntityBackend val ~ SqlBackend) => UserId -> EntityField val SchoolId -> EntityField val UserId -> DB [SchoolName]
getSchoolsOf uid uschool uuser = fmap (Import.map E.unValue) $ E.select $ E.from $ \(urights `E.InnerJoin` school) -> do
E.on $ urights E.^. uschool E.==. school E.^. SchoolId
E.where_ $ urights E.^. uuser E.==. E.val uid
E.orderBy [E.asc $ school E.^.SchoolName]
return $ school E.^. SchoolName

View File

@ -90,9 +90,9 @@ getDeleteR DeleteRoute{..} = do
(deleteFormWdgt, deleteFormEnctype) <- generateFormPost $ confirmForm' drRecords confirmString
Just targetRoute <- getCurrentRoute
sendResponse =<<
defaultLayout $(widgetFile "widgets/delete-confirmation")
defaultLayout $(widgetFile "widgets/delete-confirmation/delete-confirmation")

View File

@ -179,6 +179,9 @@ pointsFieldMax :: (Monad m, HandlerSite m ~ UniWorX) => Maybe Points -> Field m
pointsFieldMax Nothing = pointsField
pointsFieldMax (Just maxp) = checkBool (<= maxp) (MsgPointsTooHigh maxp) pointsField
matriculationField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Text
matriculationField = textField -- no restrictions, since not everyone has a matriculation and pupils need special tags here
termsActiveField :: Field Handler TermId
termsActiveField = selectField $ optionsPersistKey [TermActive ==. True] [Desc TermStart] termName
@ -588,9 +591,9 @@ multiAction acts defAction = do
widgets <- mapM mToWidget results
let actionWidgets = Map.foldrWithKey accWidget [] widgets
accWidget _act Nothing = id
accWidget act (Just w) = cons $(widgetFile "widgets/multiAction")
accWidget act (Just w) = cons $(widgetFile "widgets/multi-action/multi-action")
actionResults = Map.map fst results
return ((actionResults Map.!) =<< actionRes, $(widgetFile "widgets/multiActionCollect"))
return ((actionResults Map.!) =<< actionRes, $(widgetFile "widgets/multi-action/multi-action-collect"))
multiActionA :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action)
=> FieldSettings UniWorX

View File

@ -25,7 +25,7 @@ gradeSummaryWidget title sts =
hasMarkedPasses = positiveSum $ numMarkedPasses sumSummaries
hasPoints = positiveSum $ numSheetsPoints sumSummaries
hasMarkedPoints = positiveSum $ numMarkedPoints sumSummaries
rowWdgts = [ $(widgetFile "widgets/gradingSummaryRow")
rowWdgts = [ $(widgetFile "widgets/grading-summary/grading-summary-row")
| (sumHeader,summary) <-
[ (MsgSheetTypeNormal' ,normalSummary)
, (MsgSheetTypeBonus' ,bonusSummary)
@ -33,4 +33,4 @@ gradeSummaryWidget title sts =
] ]
in if 0 == numSheets sumSummaries
then mempty
else $(widgetFile "widgets/gradingSummary")
else $(widgetFile "widgets/grading-summary/grading-summary")

View File

@ -34,6 +34,18 @@ timeCell t = cell $ formatTime SelFormatDateTime t >>= toWidget
userCell :: IsDBTable m a => Text -> Text -> DBCell m a
userCell displayName surname = cell $ nameWidget displayName surname
cellHasUser :: (IsDBTable m a, HasUser c) => c -> DBCell m a
cellHasUser = liftA2 userCell (view _userDisplayName) (view _userSurname)
cellHasUserLink :: (IsDBTable m a, HasEntity u User) => (CryptoUUIDUser -> Route UniWorX) -> u -> DBCell m a
cellHasUserLink toLink user =
let uid = user ^. _entityKey
nWdgt = nameWidget (user ^. _entityVal . _userDisplayName) (user ^. _entityVal . _userSurname)
in anchorCellM (toLink <$> encrypt uid) nWdgt
cellHasMatrikelnummer :: (IsDBTable m a, HasUser c) => c -> DBCell m a
cellHasMatrikelnummer = maybe mempty textCell . view _userMatrikelnummer
-- Just for documentation purposes; inline this code instead:
maybeTimeCell :: IsDBTable m a => Maybe UTCTime -> DBCell m a
maybeTimeCell = maybe mempty timeCell
@ -110,3 +122,10 @@ correctorStateCell sc =
correctorLoadCell :: IsDBTable m a => SheetCorrector -> DBCell m a
correctorLoadCell sc =
i18nCell $ sheetCorrectorLoad sc
commentCell :: IsDBTable m a => Maybe (Route UniWorX) -> DBCell m a
commentCell Nothing = mempty
commentCell (Just link) = anchorCell link icon
where
icon = commentWidget True

View File

@ -576,7 +576,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
, let t' = toPathPiece $ SortingSetting t d
]
wIdent :: Text -> Text
wIdent = toPathPiece . WithIdent dbtIdent
wIdent = toPathPiece . WithIdent dbtIdent
dbsAttrs'
| not $ null dbtIdent = ("id", dbtIdent) : dbsAttrs
| otherwise = dbsAttrs
@ -802,7 +802,7 @@ cellTooltip :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a -
cellTooltip msg = cellContents.mapped %~ (<> tipWdgt)
where
tipWdgt = [whamlet|
<div .js-tooltip>
<div .tooltip>
<div .tooltip__handle>
<div .tooltip__content>_{msg}
|]

View File

@ -9,7 +9,7 @@ modal modalTrigger modalContent = do
let modalDynamic = isLeft modalContent
modalId <- newIdent
triggerId <- newIdent
$(widgetFile "widgets/modal")
$(widgetFile "widgets/modal/modal")
case modalContent of
Left route -> do
route' <- toTextUrl route

View File

@ -2,7 +2,9 @@ module Jobs.Handler.QueueNotification
( dispatchJobQueueNotification
) where
import Import
import Import hiding ((\\))
import Data.List ((\\))
import Jobs.Types
@ -20,7 +22,7 @@ dispatchJobQueueNotification jNotification = runDBJobs . setSerializable $ do
guard $ notificationAllowed userNotificationSettings nClass
return uid
determineNotificationCandidates :: Notification -> DB [Entity User]
determineNotificationCandidates NotificationSubmissionRated{..}
= E.select . E.from $ \(user `E.InnerJoin` submissionUser) -> do
@ -40,20 +42,34 @@ determineNotificationCandidates NotificationSheetSoonInactive{..}
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
return user
determineNotificationCandidates NotificationSheetInactive{..}
= E.select . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` sheet) -> do
E.on $ lecturer E.^. LecturerCourse E.==. sheet E.^. SheetCourse
= E.select . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` sheet) -> do
E.on $ lecturer E.^. LecturerCourse E.==. sheet E.^. SheetCourse
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
return user
determineNotificationCandidates NotificationCorrectionsAssigned{..}
= selectList [UserId ==. nUser] []
= selectList [UserId ==. nUser] []
determineNotificationCandidates NotificationCorrectionsNotDistributed{nSheet}
= E.select . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` sheet) -> do
E.on $ lecturer E.^. LecturerCourse E.==. sheet E.^. SheetCourse
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
return user
determineNotificationCandidates NotificationUserRightsUpdate{..}
= do
-- always send to affected user
affectedUser <- selectList [UserId ==. nUser] []
-- send to same-school admins only if there was an update
currentAdminSchools <- fmap (userAdminSchool . entityVal) <$> selectList [UserAdminUser ==. nUser] []
let oldAdminSchools = [ SchoolKey ssh | (ssh, True, _) <- originalRights ]
newAdminSchools = currentAdminSchools \\ oldAdminSchools
affectedAdmins <- E.select . E.from $ \(user `E.InnerJoin` admin) -> do
E.on $ admin E.^. UserAdminUser E.==. user E.^. UserId
E.where_ $ admin E.^. UserAdminSchool `E.in_` E.valList newAdminSchools
return user
return $ affectedUser <> affectedAdmins
classifyNotification :: Notification -> DB NotificationTrigger
classifyNotification NotificationSubmissionRated{..} = do
Sheet{sheetType} <- belongsToJust submissionSheet =<< getJust nSubmission
@ -65,5 +81,6 @@ classifyNotification NotificationSheetSoonInactive{} = return NTSheetSoonInactiv
classifyNotification NotificationSheetInactive{} = return NTSheetInactive
classifyNotification NotificationCorrectionsAssigned{} = return NTCorrectionsAssigned
classifyNotification NotificationCorrectionsNotDistributed{} = return NTCorrectionsNotDistributed
classifyNotification NotificationUserRightsUpdate{} = return NTUserRightsUpdate

View File

@ -12,6 +12,7 @@ import Jobs.Handler.SendNotification.SheetActive
import Jobs.Handler.SendNotification.SheetInactive
import Jobs.Handler.SendNotification.CorrectionsAssigned
import Jobs.Handler.SendNotification.CorrectionsNotDistributed
import Jobs.Handler.SendNotification.UserRightsUpdate
dispatchJobSendNotification :: UserId -> Notification -> Handler ()

View File

@ -12,6 +12,8 @@ import Handler.Utils.Mail
import Text.Hamlet
import qualified Data.CaseInsensitive as CI
import qualified Database.Esqueleto as E
dispatchNotificationSheetSoonInactive :: SheetId -> UserId -> Handler ()
dispatchNotificationSheetSoonInactive nSheet jRecipient = userMailT jRecipient $ do
(Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do
@ -33,10 +35,16 @@ dispatchNotificationSheetSoonInactive nSheet jRecipient = userMailT jRecipient $
dispatchNotificationSheetInactive :: SheetId -> UserId -> Handler ()
dispatchNotificationSheetInactive nSheet jRecipient = userMailT jRecipient $ do
(Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do
(Course{..}, Sheet{..}, nrSubs, nrSubmitters) <- liftHandlerT . runDB $ do
sheet <- getJust nSheet
course <- belongsToJust sheetCourse sheet
return (course, sheet)
nrSubs <- count [SubmissionSheet ==. nSheet]
(E.Value nrSubmitters:_) <- E.select . E.from $ \(subUser `E.InnerJoin` submission) -> do
E.on $ subUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
E.where_ $ submission E.^. SubmissionSheet E.==. E.val nSheet
-- E.distinctOn [E.don (subUser E.^. SubmissionUserUser)] -- Not necessary due to UniqueSubmisionUser
return (E.countRows :: E.SqlExpr (E.Value Int64))
return (course, sheet, nrSubs, nrSubmitters)
setSubjectI $ MsgMailSubjectSheetInactive courseShorthand sheetName
MsgRenderer mr <- getMailMsgRenderer
@ -49,4 +57,4 @@ dispatchNotificationSheetInactive nSheet jRecipient = userMailT jRecipient $ do
addAlternatives $ do
let editNotifications = $(ihamletFile "templates/mail/editNotifications.hamlet")
providePreferredAlternative ($(ihamletFile "templates/mail/sheetInactive.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))

View File

@ -0,0 +1,27 @@
{-# OPTIONS_GHC -fno-warn-unused-do-bind #-} -- ihamletFile discards do results
module Jobs.Handler.SendNotification.UserRightsUpdate
( dispatchNotificationUserRightsUpdate
) where
import Import
import Handler.Utils.Database
import Handler.Utils.Mail
import Text.Hamlet
-- import qualified Data.CaseInsensitive as CI
dispatchNotificationUserRightsUpdate :: UserId -> [(SchoolShorthand,Bool,Bool)]-> UserId -> Handler ()
dispatchNotificationUserRightsUpdate nUser _originalRights jRecipient = userMailT jRecipient $ do
(User{..}, adminSchools, lecturerSchools) <- liftHandlerT . runDB $ do
user <-getJust nUser
adminSchools <- getSchoolsOf nUser UserAdminSchool UserAdminUser
lecturerSchools <- getSchoolsOf nUser UserLecturerSchool UserLecturerUser
return (user,adminSchools,lecturerSchools)
setSubjectI $ MsgMailSubjectUserRightsUpdate userDisplayName
-- MsgRenderer mr <- getMailMsgRenderer
addAlternatives $ do
let editNotifications = $(ihamletFile "templates/mail/editNotifications.hamlet")
providePreferredAlternative ($(ihamletFile "templates/mail/userRightsUpdate.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))

View File

@ -18,7 +18,7 @@ data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notifica
| JobHelpRequest { jSender :: Either (Maybe Address) UserId
, jRequestTime :: UTCTime
, jHelpRequest :: Text, jReferer :: Maybe Text }
| JobSetLogSettings { jInstance :: InstanceId, jLogSettings :: LogSettings }
| JobSetLogSettings { jInstance :: InstanceId, jLogSettings :: LogSettings }
| JobDistributeCorrections { jSheet :: SheetId }
deriving (Eq, Ord, Show, Read, Generic, Typeable)
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
@ -27,6 +27,7 @@ data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
| NotificationSheetInactive { nSheet :: SheetId }
| NotificationCorrectionsAssigned { nUser :: UserId, nSheet :: SheetId }
| NotificationCorrectionsNotDistributed { nSheet :: SheetId }
| NotificationUserRightsUpdate { nUser :: UserId, originalRights :: [(SchoolShorthand,Bool,Bool)] } -- User rights (admin, lecturer,...) were changed somehow
deriving (Eq, Ord, Show, Read, Generic, Typeable)
instance Hashable Job

View File

@ -559,6 +559,7 @@ data NotificationTrigger = NTSubmissionRatedGraded
| NTSheetInactive
| NTCorrectionsAssigned
| NTCorrectionsNotDistributed
| NTUserRightsUpdate
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe NotificationTrigger
@ -590,6 +591,7 @@ instance Default NotificationSettings where
NTSheetInactive -> True
NTCorrectionsAssigned -> True
NTCorrectionsNotDistributed -> True
NTUserRightsUpdate -> True
instance ToJSON NotificationSettings where
toJSON v = toJSON . HashMap.fromList $ map (id &&& notificationAllowed v) universeF
@ -607,7 +609,6 @@ derivePersistFieldJSON ''NotificationSettings
instance ToBackendKey SqlBackend record => Hashable (Key record) where
hashWithSalt s key = s `hashWithSalt` fromSqlKey key
derivePersistFieldJSON ''MailLanguages

View File

@ -10,15 +10,12 @@ import qualified Data.Set as Set
import qualified Database.Esqueleto as E
-- import Database.Persist -- currently not needed here
-- ezero = E.val (0 :: Int64)
emptyOrIn :: PersistField typ =>
E.SqlExpr (E.Value typ) -> Set typ -> E.SqlExpr (E.Value Bool)
emptyOrIn criterion testSet
| Set.null testSet = E.val True
| otherwise = criterion `E.in_` E.valList (Set.toList testSet)
entities2map :: PersistEntity record => [Entity record] -> Map (Key record) record
entities2map = foldl' (\m entity -> Map.insert (entityKey entity) (entityVal entity) m) Map.empty

View File

@ -42,7 +42,7 @@ data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize
renderAForm :: Monad m => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do
(res, ($ []) -> fieldViews) <- aFormToForm aform
let widget = $(widgetFile "widgets/form")
let widget = $(widgetFile "widgets/form/form")
return (res, widget)
-- | special id to identify form section headers, see 'aformSection' and 'formSection'
@ -407,7 +407,7 @@ reorderField optList = Field{..}
isSel n = (==) (either (const $ map optionInternalValue olOptions) id val !! pred n) . optionInternalValue
nums = map (id &&& withNum theId) [1..length olOptions]
withNum t n = tshow n <> "." <> t
$(widgetFile "widgets/permutation")
$(widgetFile "widgets/permutation/permutation")
optionsFinite :: ( MonadHandler m
, Finite a

View File

@ -3,7 +3,7 @@ module Utils.Lens ( module Utils.Lens ) where
import Import.NoFoundation
import Control.Lens as Utils.Lens hiding ((<.>))
import Control.Lens.Extras as Utils.Lens (is)
import Utils.Lens.TH as Utils.Lens (makeLenses_)
import Utils.Lens.TH as Utils.Lens (makeLenses_, makeClassyFor_)
import qualified Database.Esqueleto as E (Value(..),InnerJoin(..))
@ -26,9 +26,35 @@ _InnerJoinRight :: Lens' (E.InnerJoin l r) r
_InnerJoinRight f (E.InnerJoin l r) = (l `E.InnerJoin`) <$> f r
makeLenses_ ''Entity
-- makeLenses_ ''Entity
makeClassyFor_ "HasEntity" "hasEntity" ''Entity
-- class HasEntity c record | c -> record where
-- hasEntity :: Lens' c (Entity record)
-- makeLenses_ ''Course
makeClassyFor_ "HasCourse" "hasCourse" ''Course
-- class HasCourse c where
-- hasCourse :: Lens' c Course
instance (HasCourse a) => HasCourse (Entity a) where
hasCourse = _entityVal . hasCourse
makeClassyFor_ "HasUser" "hasUser" ''User
-- > :info HasUser
-- class HasUser c where {-# MINIMAL hasUser #-}
-- hasUser :: Lens' c User
-- _userDisplayName :: Lens' c Text
-- _userSurname :: Lens' c Text
-- _user...
--
-- TODO: Is this instance needed?
instance (HasUser a) => HasUser (Entity a) where
hasUser = _entityVal . hasUser
-- This is what we would want instead:
-- instance (HasEntity a User) => HasUser a where
-- hasUser = _entityVal
makeLenses_ ''Course
makeLenses_ ''SheetCorrector

View File

@ -1,5 +1,6 @@
module Utils.Lens.TH where
import ClassyPrelude (String, Maybe(..))
import Control.Lens
import Control.Lens.Internal.FieldTH
import Language.Haskell.TH
@ -15,6 +16,13 @@ lensRules_ :: LensRules
lensRules_ = lensRules
& lensField .~ \_ _ n -> [TopName (mkName ('_':nameBase n))]
-- | Like lensRules_, but different class and function name
classyRulesFor_ :: ClassyNamer -> LensRules
classyRulesFor_ clsNamer = classyRules
& lensClass .~ clsNamer
& lensField .~ \_ _ n -> [TopName (mkName ('_':nameBase n))]
-- | Build lenses (and traversals) with a sensible default configuration.
-- Works the same as 'makeLenses' except that
-- the resulting lens is also prefixed with an underscore.
@ -42,6 +50,14 @@ lensRules_ = lensRules
-- @
-- 'makeLenses_' = 'makeLensesWith' 'lensRules_'
-- @
makeLenses_ :: Name -> DecsQ
makeLenses_ = makeFieldOptics lensRules_
-- | like makeClassyFor but only specifies names for class and its function,
-- otherwise lenses are created with underscore like `makeLenses_`
makeClassyFor_ :: String -> String -> Name -> DecsQ
makeClassyFor_ clsName funName = makeFieldOptics (classyRulesFor_ clNamer)
where
clNamer :: ClassyNamer
-- clNamer _ = Just (clsName, funName) -- for newer versions >= 4.17
clNamer _ = Just (mkName clsName, mkName funName)

View File

@ -1,40 +1,45 @@
# Datei Index
Database,Esqueleto.*
: Hilfsdefinitionen, welche Esqueleto anbieten könnte
Utils, Utils.*
: Hilfsfunktionionen _unabhängig von Foundation_
Utils
: Yesod Hilfsfunktionen und Instanzen, Text-HTML-Widget-Konvertierungen
(`DisplayAble`), Crud, `NTop`, Utility-Funktionen für `MonadPlus`, `Maybe`,
`MaybeT`, `Map`, und Attrs-Lists
Utils.TH
: Template Haskell code-generatoren von unabhängigen Hilfsfunktionen (`deriveSimpleWith`)
Utils.DB
: Derived persistent functions (`existsBy`, `getKeyBy404`, ...)
Utils.Form
: `renderAForm`, Field-Settings helper, `FormIdentifier`, `Button`-Klasse,
unabhängige konkrete Buttons
Utils.PathPiece
: (Template-Haskell)-Hilfsfunktionen für Formulierung von PathPiece-Instanzen
Utils.Message
: redefines addMessage, addMessageI, defines MessageClass
Utils.Lens
: Automatisch erzeugt Linsen für eigene und Yesod-Typen, `Control.Lens`-Export
Utils.DateTime
: Template Haskell code-generatoren zum compile-time einbinden von Zeitzone
und `TimeLocale`
Handler.Utils, Handler.Utils.*
: Hilfsfunktionien, importieren `Import`
Handler.Utils
: `Handler.Utils.*`, Unsortierte _Foundation-abhängige_ Hilfsfunktionen
Handler.Utils.DateTime
: Nutzer-spezifisches `DateTime`-Formatieren
@ -42,39 +47,39 @@ Handler.Utils.Form
: Konkrete Buttons, spezielle Felder (inkl. Datei-Upload-Felder),
Optionslisten (`optionsPersistCryptoId`), `forced`-Felder (erzwungenes
Ergebnis, deaktiviertes Feld), `multiAction`
Handler.Utils.Rating
: `Rating` (kodiert eine Rating-Datei), Parsen und PrettyPrinten von
Rating-Dateien
Handler.Utils.Sheet
: `fetchSheet`
Handler.Utils.StudyFeatures
: Parsen von LDAP StudyFeatures-Strings
Handler.Utils.Submission
: `assignSubmissions`, `sinkSubmission` State-Maschinen die (bereits geparste)
ZIP-Archive auseinandernehmen und (in einer Transaction) in die Datenbank
speichern
Handler.Utils.Submission.TH
: Template Haskell zum parsen und einkompilieren von Dateiname-Blacklist für
`sinkSubmission`; Patterns in `config/submission-blacklist`
Handler.Utils.Table
: Hilfsfunktion zum direkten Benutzen von Colonnade (kein `dbTable`)
Handler.Utils.Table.Pagination
: Here be Dragons
Paginated database-backed tables with support for sorting, filtering,
numbering, forms, further database-requests within cells
Includes helper functions for mangling pagination-, sorting-, and filter-settings
Includes helper functions for constructing common types of cells
Handler.Utils.Table.Pagination.Types
: `Sortable`-Headedness for colonnade
@ -83,17 +88,17 @@ Handler.Utils.Table.Cells
Handler.Utils.Templates
: Modals
Handler.Utils.Zip
: Conduit-basiertes ZIP Parsen und Erstellen
Handler.Common
: Handler aus dem Scaffolding; Implementierungen von Handlern, die _jede
Website_ irgendwann braucht
CryptoID
: Definiert CryptoIDs für custom Typen (aus Model)
Model.Migration
: Manuelle Datenbank-Migration
@ -103,43 +108,43 @@ Model.Rating
Jobs
: `handleJobs` worker thread handling background jobs
`JobQueueException`
Jobs.Types
: `Job`, `Notification`, `JobCtl` Types of Jobs
Cron.Types
: Datentypen zur Spezifikation von Intervallen zu denen Jobs ausgeführt werden
können:
`Cron`, `CronMatch`, `CronAbsolute`, `CronRepeat`, `Crontab`
Cron
: Seiteneffektfreie Berechnungen auf Typen aus `Cron.Types`: `nextCronMatch`
Jobs.Queue
: Funktionen zum _anstoßen_ von Jobs und zur Kommunikation mit den
Worker-Threads
`writeJobCtl` schickt Nachricht an einen pseudo-Zufälligen worker-thread der
lokalen Instanz
`queueJob` und `queueJob'` schreiben neue Jobs in die Instanz-übergreifende
Job-Queue, `queueJob'` stößt außerdem einen lokalen worker-thread an sich
des Jobs anzunehmen
`runDBJobs` ersetzt `runDB` und erlaubt `queueDBJob` zu
benutzen. `queueDBJob` schreibt einen Job in die Queue; am Ende stößt
`runDBJobs` lokale worker-threads für alle mit `queueDBJobs` eingetragenen
Jobs an.
Jobs.TH
: Templatehaskell für den dispatch mechanismus für `Jobs`
Jobs.Crontab
: Generiert `Crontab JobCtl` aus der Datenbank (sammelt alle in den Daten aus
der Datenbank impliziten Jobs (notifications zu bestimmten zeiten,
aufräumaktionen, ...) ein)
Jobs.Handler.**
: Via `Jobs.TH` delegiert `Jobs` das Interpretieren und Ausführen eines Werts
aus `Jobs.Types` an einen dieser Handler

View File

@ -4,12 +4,11 @@
z-index: 1;
top: 0;
left: 0;
flex: 0 0 0;
flex-basis: var(--asidenav-width-lg, 20%);
min-height: calc(100% - var(--header-height));
transition: all .2s ease-out;
width: var(--asidenav-width-lg, 20%);
height: 100%;
flex: 0 0 0;
flex-basis: var(--asidenav-width-lg, 20%);
transition: all .2s ease-out;
&::before {
position: absolute;
@ -72,6 +71,14 @@
.asidenav {
color: var(--color-font);
min-height: calc(100% - var(--header-height));
height: 400px;
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 0;
}
}
.asidenav__box {
@ -183,9 +190,7 @@
/* LIST-ITEM */
.asidenav__list-item {
position: relative;
color: var(--color-font);
min-height: 50px;
display: flex;
flex-direction: column;
justify-content: flex-start;
@ -207,10 +212,8 @@
text-shadow: none;
}
.asidenav__nested-list {
transform: translateX(100%);
opacity: 1;
width: 200px;
.asidenav__nested-list-wrapper {
display: block;
}
}
}
@ -242,7 +245,7 @@
display: flex;
flex: 1;
align-items: center;
padding: 7px 10px;
padding: 8px 3px;
justify-content: flex-start;
color: var(--color-font);
width: 100%;
@ -258,17 +261,17 @@
}
/* hover sub-menus */
.asidenav__nested-list {
.asidenav__nested-list-wrapper {
position: absolute;
top: 0;
right: 0;
z-index: 10;
display: none;
color: var(--color-font);
transform: translateX(0);
opacity: 0;
width: 0;
overflow: hidden;
z-index: -1;
box-shadow: 0 0 13px rgba(0, 0, 0, 0.4);
background-color: var(--color-grey-light);
box-shadow: 1px 1px 1px 0px var(--color-grey);
}
.asidenav__nested-list {
min-width: 200px;
}
@media (max-width: 425px) {
@ -280,19 +283,16 @@
.asidenav__nested-list-item {
position: relative;
color: var(--color-lightwhite);
background-color: var(--color-dark);
&:hover {
background-color: var(--color-darker);
background-color: var(--color-lightwhite);
}
.asidenav__link-wrapper {
padding-left: 13px;
padding-right: 13px;
border-left: 20px solid white;
transition: all .2s ease;
color: var(--color-lightwhite);
color: var(--color-font);
}
}

View File

@ -1,7 +1,3 @@
.js-show-hide {
position: relative;
}
.js-show-hide__toggle {
position: relative;
cursor: pointer;

View File

@ -17,7 +17,7 @@
text-align: center;
padding: 0 13px;
margin: 0 2px;
background-color: #b3b7c1;
background-color: var(--color-dark);
color: white;
font-size: 16px;
text-transform: uppercase;
@ -35,5 +35,5 @@
.tab-opener.tab-visible {
background-color: transparent;
color: rgb(52, 48, 58);
border-bottom-color: #5F98C2;
border-bottom-color: var(--color-primary);
}

View File

@ -1,4 +1,4 @@
.js-tooltip {
.tooltip {
position: relative;
display: inline-block;
height: 1.5rem;
@ -67,7 +67,7 @@
@media (max-width: 768px) {
.js-tooltip {
.tooltip {
display: block;
margin-top: 10px;

View File

@ -3,6 +3,7 @@
window.utils = window.utils || {};
var ALERTS_CLASS = 'alerts';
var ALERTS_TOGGLER_CLASS = 'alerts__toggler';
var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible';
var ALERTS_TOGGLER_APPEAR_DELAY = 120;
@ -18,7 +19,11 @@
window.utils.alerts = function(alertsEl) {
if (alertsEl.classList.contains(JS_INITIALIZED_CLASS)) {
return;
return false;
}
if (!alertsEl || !alertsEl.classList.contains(ALERTS_CLASS)) {
throw new Error('utils.alerts has to be called with alerts element');
}
var togglerCheckRequested = false;

View File

@ -0,0 +1,59 @@
(function() {
'use strict';
window.utils = window.utils || {};
var FAVORITES_BTN_CLASS = 'navbar__list-item--favorite';
var FAVORITES_BTN_ACTIVE_CLASS = 'navbar__list-item--active';
var ASIDENAV_EXPANDED_CLASS = 'main__aside--expanded';
var ASIDENAV_LIST_ITEM_CLASS = 'asidenav__list-item';
var ASIDENAV_SUBMENU_CLASS = 'asidenav__nested-list-wrapper';
window.utils.aside = function(asideEl) {
if (!asideEl) {
throw new Error('asideEl not defined');
}
function initFavoritesButton() {
var favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS);
favoritesBtn.addEventListener('click', function(event) {
favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS);
asideEl.classList.toggle(ASIDENAV_EXPANDED_CLASS);
event.preventDefault();
}, true);
}
function initAsidenavSubmenus() {
var asidenavLinksWithSubmenus = Array.from(asideEl.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS))
.map(function(listItem) {
var submenu = listItem.querySelector('.' + ASIDENAV_SUBMENU_CLASS);
return { listItem, submenu };
}).filter(function(union) {
return union.submenu !== null;
});
asidenavLinksWithSubmenus.forEach(function(union) {
union.listItem.addEventListener('mouseover', createMouseoverHandler(union));
});
}
function createMouseoverHandler(union) {
return function mouseoverHanlder(event) {
var rectListItem = union.listItem.getBoundingClientRect();
var rectSubMenu = union.submenu.getBoundingClientRect();
union.submenu.style.left = (rectListItem.left + rectListItem.width) + 'px';
if (window.innerHeight - rectListItem.top < rectSubMenu.height) {
union.submenu.style.top = (rectListItem.top + rectListItem.height - rectSubMenu.height) + 'px';
} else {
union.submenu.style.top = rectListItem.top + 'px';
}
};
}
initFavoritesButton();
initAsidenavSubmenus();
};
})();

View File

@ -0,0 +1,202 @@
(function collonadeClosure() {
'use strict';
window.utils = window.utils || {};
var HEADER_HEIGHT = 80;
var RESET_OPTIONS = [ 'scrollTo' ];
window.utils.asyncTable = function(wrapper, options) {
options = options || {};
var tableIdent = options.dbtIdent;
var shortCircuitHeader = options ? options.headerDBTableShortcircuit : null;
var ths = [];
var pageLinks = [];
var pagesizeForm;
var scrollTable;
function init() {
var table = wrapper.querySelector('#' + tableIdent);
if (!table) {
return;
}
scrollTable = wrapper.querySelector('.scrolltable');
// sortable table headers
ths = Array.from(table.querySelectorAll('th.sortable')).map(function(th) {
return { element: th };
});
// pagination links
var pagination = wrapper.querySelector('#' + tableIdent + '-pagination');
if (pagination) {
pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) {
return { element: link };
});
}
// pagesize form
pagesizeForm = wrapper.querySelector('#' + tableIdent + '-pagesize-form');
// take options into account
if (options && options.scrollTo) {
window.scrollTo(options.scrollTo);
}
if (options && options.horizPos && scrollTable) {
scrollTable.scrollLeft = options.horizPos;
}
setupListeners();
wrapper.classList.add('js-initialized');
}
function setupListeners() {
ths.forEach(function(th) {
th.clickHandler = function(event) {
var boundClickHandler = clickHandler.bind(this);
var horizPos = (scrollTable || {}).scrollLeft;
boundClickHandler(event, { horizPos });
};
th.element.addEventListener('click', th.clickHandler);
});
pageLinks.forEach(function(link) {
link.clickHandler = function(event) {
var boundClickHandler = clickHandler.bind(this);
var tableBoundingRect = scrollTable.getBoundingClientRect();
var tableOptions = {};
if (tableBoundingRect.top < HEADER_HEIGHT) {
tableOptions.scrollTo = {
top: (scrollTable.offsetTop || 0) - HEADER_HEIGHT,
left: scrollTable.offsetLeft || 0,
behavior: 'smooth',
};
}
boundClickHandler(event, tableOptions);
}
link.element.addEventListener('click', link.clickHandler);
});
if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]');
pagesizeSelect.addEventListener('change', changePagesizeHandler);
}
}
function removeListeners() {
ths.forEach(function(th) {
th.element.removeEventListener('click', th.clickHandler);
});
pageLinks.forEach(function(link) {
link.element.removeEventListener('click', link.clickHandler);
});
if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]')
pagesizeSelect.removeEventListener('change', changePagesizeHandler);
}
}
function clickHandler(event, tableOptions) {
event.preventDefault();
var url = new URL(window.location.origin + window.location.pathname + getClickDestination(this));
updateTableFrom(url, tableOptions);
}
function getClickDestination(el) {
if (!el.querySelector('a')) {
return '';
}
return el.querySelector('a').getAttribute('href');
}
function changePagesizeHandler(event) {
var currentTableUrl = options.currentUrl || window.location.href;
var url = getUrlWithUpdatedPagesize(currentTableUrl, event.target.value);
url = new URL(getUrlWithResetPagenumber(url));
updateTableFrom(url);
}
function getUrlWithUpdatedPagesize(url, pagesize) {
if (url.indexOf('pagesize') >= 0) {
return url.replace(/pagesize=(\d+|all)/, 'pagesize=' + pagesize);
} else if (url.indexOf('?') >= 0) {
return url += '&' + tableIdent + '-pagesize=' + pagesize;
}
return url += '?' + tableIdent + '-pagesize=' + pagesize;
}
function getUrlWithResetPagenumber(url) {
return url.replace(/-page=\d+/, '-page=0');
}
// fetches new sorted table from url with params and replaces contents of current table
function updateTableFrom(url, tableOptions) {
tableOptions = tableOptions || {};
fetch(url, {
credentials: 'same-origin',
headers: {
'Accept': 'text/html',
[shortCircuitHeader]: tableIdent
}
}).then(function(response) {
if (!response.ok) {
throw new Error('Looks like there was a problem fetching ' + url.href + '. Status Code: ' + response.status);
}
return response.text();
}).then(function(data) {
tableOptions.currentUrl = url.href;
removeListeners();
updateWrapperContents(data, tableOptions);
}).catch(function(err) {
console.error(err);
});
}
function updateWrapperContents(newHtml, tableOptions) {
tableOptions = tableOptions || {};
wrapper.innerHTML = newHtml;
wrapper.classList.remove("js-initialized");
// setup the wrapper and its components to behave async again
window.utils.teardown('asyncTable');
window.utils.teardown('form');
// merge global options and table specific options
var resetOptions = {};
Object.keys(options)
.filter(function(key) {
return !RESET_OPTIONS.includes(key);
})
.forEach(function(key) {
resetOptions[key] = options[key];
});
var combinedOptions = {};
combinedOptions = Object.keys(tableOptions)
.filter(function(key) {
return tableOptions.hasOwnProperty(key);
})
.map(function(key) {
return { key, value: tableOptions[key] }
})
.reduce(function(cumulatedOpts, opt) {
cumulatedOpts[opt.key] = opt.value;
return cumulatedOpts;
}, resetOptions);
window.utils.setup('asyncTable', wrapper, combinedOptions);
Array.from(wrapper.querySelectorAll('form')).forEach(function(form) {
window.utils.setup('form', form);
});
}
init();
};
})();

View File

@ -1,4 +0,0 @@
window.addEventListener('touchstart', function onFirstTouch() {
document.body.classList.add('touch-supported');
window.removeEventListener('touchstart', onFirstTouch, false);
}, false);

View File

@ -3,13 +3,57 @@
window.utils = window.utils || {};
var JS_INITIALIZED = 'js-initialized';
var SUBMIT_BUTTON_SELECTOR = '[type="submit"]:not([formnovalidate])';
var AUTOSUBMIT_BUTTON_SELECTOR = '[type="submit"][data-autosubmit]';
function formValidator(inputs) {
var done = true;
inputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
}
window.utils.form = function(form, options) {
if (form.classList.contains(JS_INITIALIZED)) {
return false;
}
form.classList.add(JS_INITIALIZED);
// reactive buttons
var submitBtn = form.querySelector(SUBMIT_BUTTON_SELECTOR);
if (submitBtn) {
window.utils.setup('reactiveButton', form, { button: submitBtn });
}
// conditonal fieldsets
var fieldSets = Array.from(form.querySelectorAll('fieldset[data-conditional-id][data-conditional-value]'));
window.utils.setup('interactiveFieldset', form, { fieldSets });
// hide autoSubmit submit button
window.utils.setup('autoSubmit', form, options);
};
// registers input-listener for each element in <inputs> (array) and
// enables <button> if <validation> for these inputs returns true
window.utils.reactiveButton = function(form, button, validation) {
// enables <button> if <formValidator> for these inputs returns true
window.utils.reactiveButton = function(form, options) {
options = options || {};
var button = options.button;
var requireds = Array.from(form.querySelectorAll('[required]'));
if (!button) {
throw new Error('Please provide both a button to reactiveButton');
}
if (requireds.length == 0) {
return false;
}
if (typeof button.dataset.formnorequired !== 'undefined' && button.dataset.formnorequired !== null) {
button.addEventListener('click', function() {
form.submit();
@ -27,7 +71,7 @@
});
function updateButtonState() {
if (validation.call(null, requireds) === true) {
if (formValidator(requireds) === true) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', 'true');
@ -35,7 +79,14 @@
}
};
window.utils.interactiveFieldset = function(form, fieldSets) {
window.utils.interactiveFieldset = function(form, options) {
options = options || {};
var fieldSets = options.fieldSets;
if (!fieldSets) {
throw new Error('interactiveFieldset must be passed fieldSets via options');
}
var fields = fieldSets.map(function(fs) {
return {
fieldSet: fs,
@ -64,58 +115,11 @@
updateFields();
}
};
window.utils.autoSubmit = function(form, options) {
var button = form.querySelector(AUTOSUBMIT_BUTTON_SELECTOR);
if (button) {
button.classList.add('hidden');
}
};
})();
document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'showHide')
return;
var forms = e.detail.scope.querySelectorAll('form');
Array.from(forms).forEach(function(form) {
// auto reactiveButton submit-buttons with required fields
var submitBtns = Array.from(form.querySelectorAll('[type=submit]:not([formnovalidate])'));
submitBtns.forEach(function(submitBtn) {
window.utils.reactiveButton(form, submitBtn, validateForm);
});
// auto conditonal fieldsets
var fieldSets = Array.from(form.querySelectorAll('fieldset[data-conditional-id][data-conditional-value]'));
window.utils.interactiveFieldset(form, fieldSets);
});
function validateForm(inputs) {
var done = true;
inputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
}
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'showHide' }, bubbles: true, cancelable: true }))
});
document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'autoSubmit')
return;
Array.from(e.detail.scope.querySelectorAll('[data-autosubmit]:not(.js-initialized)')).forEach(function(elem) {
if ((elem instanceof HTMLInputElement && elem.type == 'submit') || (elem instanceof HTMLButtonElement && elem.type == 'submit')) {
var ancestor = elem.closest('.form-group');
var target = ancestor || elem;
target.classList.add('hidden');
}
elem.classList.add('js-initialized');
});
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'autoSubmit' }, bubbles: true, cancelable: true }))
});

171
static/js/utils/inputs.js Normal file
View File

@ -0,0 +1,171 @@
(function() {
'use strict';
window.utils = window.utils || {};
var JS_INITIALIZED_CLASS = 'js-initialized';
function isNotInitialized(element) {
return !element.classList.contains(JS_INITIALIZED_CLASS);
}
window.utils.inputs = function(wrapper, options) {
// checkboxes / radios
var checkboxes = Array.from(wrapper.querySelectorAll('input[type="checkbox"], input[type="radio"]'));
checkboxes.filter(isNotInitialized).forEach(window.utils.checkboxRadio);
// file-uploads
var fileUploads = Array.from(wrapper.querySelectorAll('input[type="file"]'));
fileUploads.filter(isNotInitialized).forEach(function(input) {
window.utils.fileUpload(input, options);
});
// file-checkboxes
var fileCheckboxes = Array.from(wrapper.querySelectorAll('.file-checkbox'));
fileCheckboxes.filter(isNotInitialized).forEach(function(inp) {
window.utils.fileCheckbox(inp);
inp.classList.add(JS_INITIALIZED_CLASS);
});
};
// (multiple) dynamic file uploads
// expects i18n object with following strings:
// »filesSelected«: label of multi-upload button after selection
// example: "Dateien ausgewählt" (will be prepended by number of selected files)
// »selectFile«: label of single-upload button before selection
// example: "Datei auswählen"
// »selectFiles«: label of multi-upload button before selection
// example: "Datei(en) auswählen"
var FILE_UPLOAD_INPUT_LIST_CLASS = 'file-input__list';
var FILE_UPLOAD_INPUT_UNPACK_CHECKBOX_CLASS = 'file-input__unpack';
var FILE_UPLOAD_INPUT_LABEL_CLASS = 'file-input__label';
var FILE_UPLOAD_INPUT_HIDDEN_CLASS = 'file-input__input--hidden';
window.utils.fileUpload = function(input, options) {
var isMulti = input.hasAttribute('multiple');
var fileList = isMulti ? addFileList() : null;
var label = addFileLabel();
var i18n = options.i18n;
if (!i18n) {
throw new Error('window.utils.fileUpload(input, options) needs to be passed i18n object via options');
}
input.classList.add(JS_INITIALIZED_CLASS);
function renderFileList(files) {
fileList.innerHTML = '';
Array.from(files).forEach(function(file, index) {
var fileDisplayEl = document.createElement('li');
fileDisplayEl.innerHTML = file.name;
fileList.appendChild(fileDisplayEl);
});
}
function updateLabel(files) {
if (files.length) {
if (isMulti) {
label.innerText = files.length + ' ' + i18n.filesSelected;
} else {
label.innerHTML = files[0].name;
}
} else {
resetFileLabel();
}
}
function addFileList() {
var list = document.createElement('ol');
list.classList.add(FILE_UPLOAD_INPUT_LIST_CLASS);
var unpackEl = input.parentElement.querySelector('.' + FILE_UPLOAD_INPUT_UNPACK_CHECKBOX_CLASS);
if (unpackEl) {
input.parentElement.insertBefore(list, unpackEl);
} else {
input.parentElement.appendChild(list);
}
return list;
}
function addFileLabel() {
var label = document.createElement('label');
label.classList.add(FILE_UPLOAD_INPUT_LABEL_CLASS);
label.setAttribute('for', input.id);
input.parentElement.insertBefore(label, input);
return label;
}
function resetFileLabel() {
if (isMulti) {
label.innerText = i18n.selectFiles;
} else {
label.innerText = i18n.selectFile;
}
}
// initial setup
resetFileLabel();
input.classList.add(FILE_UPLOAD_INPUT_HIDDEN_CLASS);
input.addEventListener('change', function() {
input.dispatchEvent(new Event('input'));
if (isMulti) {
renderFileList(input.files);
}
updateLabel(input.files);
});
}
// to remove previously uploaded files
var FILE_UPLOAD_CONTAINER_CLASS = 'file-container';
var FILE_UPLOAD_CONTAINER_CHECKED_CLASS = 'file-container--checked';
window.utils.fileCheckbox = function(input) {
// adds eventlistener(s)
function addListener(container) {
input.addEventListener('change', function(event) {
container.classList.toggle(FILE_UPLOAD_CONTAINER_CHECKED_CLASS, this.checked);
});
}
// initial setup
function setup() {
var cont = input.parentNode;
while (cont !== document.body) {
if (cont.matches('.' + FILE_UPLOAD_CONTAINER_CLASS)) {
break;
}
cont = cont.parentNode;
}
addListener(cont);
input.classList.add(JS_INITIALIZED_CLASS);
cont.classList.add(JS_INITIALIZED_CLASS);
}
setup();
}
// turns native checkboxes and radio buttons into custom ones
window.utils.checkboxRadio = function(input) {
var type = input.getAttribute('type');
if (!input.parentElement.classList.contains(type)) {
var parentEl = input.parentElement;
var siblingEl = input.nextElementSibling;
var wrapperEl = document.createElement('div');
var labelEl = document.createElement('label');
wrapperEl.classList.add(type);
labelEl.setAttribute('for', input.id);
wrapperEl.appendChild(input);
wrapperEl.appendChild(labelEl);
input.classList.add(JS_INITIALIZED_CLASS);
if (siblingEl) {
parentEl.insertBefore(wrapperEl, siblingEl);
} else {
parentEl.appendChild(wrapperEl);
}
}
}
})();

58
static/js/utils/setup.js Normal file
View File

@ -0,0 +1,58 @@
(function() {
'use strict';
window.utils = window.utils || {};
var registeredSetupListeners = {};
window.utils.setup = function(utilType, scope, options) {
if (!utilType || !scope) {
return;
}
options = options || {};
var listener = function(event) {
if (event.detail.targetUtil !== utilType) {
return false;
}
if (options.setupFunction) {
options.setupFunction(scope, options);
} else {
var util = window.utils[utilType];
if (!util) {
throw new Error('"' + utilType + '" is not a known js util');
}
util(scope, options);
}
};
if (registeredSetupListeners[utilType] && !options.singleton) {
registeredSetupListeners[utilType].push(listener);
} else {
window.utils.teardown(utilType);
registeredSetupListeners[utilType] = [ listener ];
}
document.addEventListener('setup', listener);
document.dispatchEvent(new CustomEvent('setup', {
detail: { targetUtil: utilType, module: 'none' },
bubbles: true,
cancelable: true,
}));
};
window.utils.teardown = function(utilType) {
if (registeredSetupListeners[utilType]) {
registeredSetupListeners[utilType].forEach(function(listener) {
document.removeEventListener('setup', listener);
});
delete registeredSetupListeners[utilType];
}
}
})();

View File

@ -0,0 +1,55 @@
(function() {
'use strict';
window.utils = window.utils || {};
var LOCAL_STORAGE_SHOW_HIDE = 'SHOW_HIDE';
/**
* div
* div.js-show-hide__toggle
* toggle here
* div
* content here
*/
window.utils.showHide = function(wrapper, options) {
function addEventHandler(el) {
el.addEventListener('click', function elClickListener() {
var newState = el.parentElement.classList.toggle('js-show-hide--collapsed');
updateLSState(el.dataset.shIndex || null, newState);
});
}
function updateLSState(index, state) {
if (!index) {
return false;
}
var lsData = fromLocalStorage();
lsData[index] = state;
window.localStorage.setItem(LOCAL_STORAGE_SHOW_HIDE, JSON.stringify(lsData));
}
function collapsedStateInLocalStorage(index) {
return fromLocalStorage()[index] || null;
}
function fromLocalStorage() {
return JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_SHOW_HIDE)) || {};
}
Array
.from(wrapper.querySelectorAll('.js-show-hide__toggle'))
.forEach(function(el) {
var index = el.dataset.shIndex || null;
el.parentElement.classList.toggle(
'js-show-hide--collapsed',
collapsedStateInLocalStorage(index) || el.dataset.collapsed === 'true'
);
Array.from(el.parentElement.children).forEach(function(el) {
if (!el.classList.contains('js-show-hide__toggle')) {
el.classList.add('js-show-hide__target');
}
});
addEventHandler(el);
});
};
})();

View File

@ -0,0 +1,194 @@
<div .container>
<section>
<h2>Stand
<h3>Version 0.91 vom 22.5.2018
<p>
Die LMU unterliegt als Körperschaft des öffentlichen Rechts dem
bayerischen Datenschutzgesetz, in einigen Bereichen dem Bundesdatenschutzgesetz,
der europäischen Datenschutzgrundverordnung und den entsprechenden Datenschutz-relevanten Artikeln einzelner Fachgesetze (Telemedien, Telekomunnikation, Arbeitsrecht, usw.).
Diese Datenschutzerklärung erfüllt gegenüber den Nutzern die Informationspflichten aus den obigen Regularien.
<div .container>
<section>
<h2>Ansprechpartner
<h3>Datenschutzbeauftragter der Ludwig-Maximilians-Universität München
<ul style="list-style-type: none">
<li>Dr. Rolf Gemmeke
<li>Geschwister-Scholl-Platz 1, 80539 München
<li>Tel.: +49 (0) 89 2180-2414
<li>
<a href="http://www.uni-muenchen.de/einrichtungen/orga_lmu/beauftragte/dschutz/index.html">
Webseite des Datenschutzbeauftragten der LMU
<h3>Aufsichtsbehörde für den Datenschutz im öffentlichen Bereich
<ul style="list-style-type: none">
<li>Bayerischer Landesbeauftragter für den Datenschutz
<li>Promenade 27
<li>91522 Ansbach
<li>Telefon: +49 (0) 981 53 1300
<li>
<a href="http://www.datenschutz-bayern.de/">
Webseite des bayerischen Datenschutzbeauftragten
<h3>Datenschutzkoordinator des Instituts für Informatik der LMU
<ul style="list-style-type: none">
<li>Robert Hofer
<li>E-Mail: dsk@ifi.lmu.de
<li>Telefon: +49 (0) 89 / 2180 - 9198
<h3>Verantwortlicher für die Verarbeitung der Daten
<ul style="list-style-type: none">
<li>Ludwig-Maximilians-Universität München
<li>Geschwister-Scholl-Platz 1
<li>80539 München
<li>Telefon: +49 (0) 89 / 2180 - 0
<li>Email: praesidium@lmu.de
<li>
<p>
Die Ludwig-Maximilians-Universität München ist eine Körperschaft des Öffentlichen Rechts.
Sie wird durch den Präsidenten Prof. Dr. Bernd Huber gesetzlich vertreten.
<h4>Verantwortlicher Fachbereich
<ul style="list-style-type: none">
<li>Rechnerbetriebsgruppe des Departments "Institut für Informatik" der Ludwig-Maximilians-Universität München
<li>Oettingenstraße 67
<li>D-80538 München
<li>E-Mail: rbg@ifi.lmu.de
<li>Telefon: +49 (0) 89 / 2180 - 9198
<div .container>
<section>
<h2>Verarbeitung persönlicher Daten
<p>
Der IT Betrieb und Organisation am Institut für Informatik wird nach dem Stand
der Technik und allgm. Empfehlungen für Sicherheitsfragen und IT Betrieb geführt.
Damit werden der Schutz persönlicher Daten, als auch der nachhaltige
Betrieb der Dienste im Rahmen der Möglichkeiten gewährleistet.
<h3>1. Protokoll des Webservers
<h4>Betroffene
Jeder Nutzer dieses Webservers ist von der Erhebung und Verarbeitung der Daten betroffen.
<h4>Welche Daten werden erhoben
Der Webserver protokolliert
<ul>
<li>Pseudonymisierte IP-Adresse des Webclients des Nutzers dieses Dienstes
<li>Datum und Uhrzeit des Abrufs eines Elementes der Webseite
<li>Adresse des abgerufenen Elementes
<li>übertragene Datenmenge
<li>Info, ob der Zugriff/Abruf erfolgreich war
<li>Art und Version des Webclients
<li>gegebenenfalls Fehlermeldungen
<li>gegebenenfalls Text einer Suchanfrage
<p>
Im Falle einer Störung oder eines Sicherheitsvorfalles wird für die Dauer des Vorfalles
die Anonymisierung der IP-Adresse aufgehoben.
<h4>Zweckbindung
<p>
Die erhobenen Daten werden nur für statistische Zwecke(anonymisiert), zur Verbesserung des Angebots,
zur Analyse, Beseitigung und Abwehr von Störungen und bei Sicherheitsvorfällen verwendet.
Nur die für den Betrieb zuständigen IT Administratoren des Instituts für Informatik
haben Zugriff auf die Daten.
<h4>Rechtliche oder vertragliche Grundlagen der Datenverarbeitung
<ul>
<li>Verpflichtung zum nachhaltigen und sicheren Betrieb von IT Diensten nach Stand der Technik (TMG, TKG, DSG, EUDGV, BayrDSG, BDSG)
<li>Rechtsprechung zur Aufbewahrungsdauer und Art von Webserverprotokollen
<li>Wahrnehmung einer Aufgabe, die im öffentlichen Interesse liegt
<h4>Auskunft
<p>Erster Ansprechspartner ist der oben aufgeführte verantwortliche Fachbereich.
<h4>Löschung
<p>
Nach sieben Tagen werden Einträge des Webserverprotokolls automatisch gelöscht. Daten die wegen einer
Störung oder eines Sicherheitsvorfalles verarbeitet werden, werden nach Ende des Vorfalls gelöscht.
<h4>Zustimmung, Berichtigung, Wiederruf, Antrag auf Löschung oder Übertragung
<p>
Eine Zustimmung zur Datenverarbeitung ist auf Grund der Art der erhobenen Daten, des Verwendungszwecks,
der automatischen Löschung und der Erhebungsgrundlagen nicht nötig (DSGVO Art.6 Abs.1 e+f).
Ein Wiederrufsrecht zur Verarbeitung, Antragsrecht auf Löschung, Antragsrecht auf Berichtigung,
Antragsrecht auf Übertragung ist wegen nicht nötiger Zustimmung zur Verarbeitung bzw. Art und Nutzung der erhobenen Daten nicht gegeben.
<h4>Beschwerderecht
<p>
Nutzer können sich generell bzgl. jeder Verarbeitung oder Weitergabe von
persönlichen Daten bei der Aufsichtbehörde beschweren.
Im Fall der LMU ist dies der oben genannte bayerische Datenschutzbeauftragte.
Ansonsten können auch alle anderen oben genannten Ansprechpartner
bzgl. Beschwerden und Nachfragen kontaktiert werden.
<h4>Verpflichtung zur Teilnahme an der Verarbeitung
<p>
Der Nutzer ist bei Nutzung dieses Dienstes verpflichtet die Daten bereitzustellen und
verarbeiten zu lassen. Wir behalten uns das Recht vor,
Nutzer, die die Daten nicht bereitstellen, von der Nutzung des Dienstes auszuschließen.
<!-- CONTINUED -->
$#
$# <h3>1. Daten der Webapplikation "Uni2Work"
$#
$# <h4>Betroffene
$# Jeder Nutzer dieses Webservers ist von der Erhebung und Verarbeitung der Daten betroffen.
$#
$# <h4>Welche Daten werden erhoben
$# Der Webserver protokolliert
$# <ul>
$# <li>Pseudonymisierte IP-Adresse des Webclients des Nutzers dieses Dienstes
$# <li>Datum und Uhrzeit des Abrufs eines Elementes der Webseite
$# <li>Adresse des abgerufenen Elementes
$# <li>übertragene Datenmenge
$# <li>Info, ob der Zugriff/Abruf erfolgreich war
$# <li>Art und Version des Webclients
$# <li>gegebenenfalls Fehlermeldungen
$# <li>gegebenenfalls Text einer Suchanfrage
$# <p>
$# Im Falle einer Störung oder eines Sicherheitsvorfalles wird für die Dauer des Vorfalles
$# die Anonymisierung der IP-Adresse aufgehoben.
$#
$# <h4>Zweckbindung
$# <p>
$# Die erhobenen Daten werden nur für statistische Zwecke(anonymisiert), zur Verbesserung des Angebots,
$# zur Analyse, Beseitigung und Abwehr von Störungen und bei Sicherheitsvorfällen verwendet.
$# Nur die für den Betrieb zuständigen IT Administratoren des Instituts für Informatik
$# haben Zugriff auf die Daten.
$#
$# <h4>Rechtliche oder vertragliche Grundlagen der Datenverarbeitung
$# <ul>
$# <li>Verpflichtung zum nachhaltigen und sicheren Betrieb von IT Diensten nach Stand der Technik (TMG, TKG, DSG, EUDGV, BayrDSG, BDSG)
$# <li>Rechtsprechung zur Aufbewahrungsdauer und Art von Webserverprotokollen
$# <li>Wahrnehmung einer Aufgabe, die im öffentlichen Interesse liegt
$#
$# <h4>Auskunft
$# <p>Erster Ansprechspartner ist der oben aufgeführte verantwortliche Fachbereich.
$#
$# <h4>Löschung
$# <p>
$# Nach sieben Tagen werden Einträge des Webserverprotokolls automatisch gelöscht. Daten die wegen einer
$# Störung oder eines Sicherheitsvorfalles verarbeitet werden, werden nach Ende des Vorfalls gelöscht.
$#
$# <h4>Zustimmung, Berichtigung, Wiederruf, Antrag auf Löschung oder Übertragung
$# <p>
$# Eine Zustimmung zur Datenverarbeitung ist auf Grund der Art der erhobenen Daten, des Verwendungszwecks,
$# der automatischen Löschung und der Erhebungsgrundlagen nicht nötig (DSGVO Art.6 Abs.1 e+f).
$# Ein Wiederrufsrecht zur Verarbeitung, Antragsrecht auf Löschung, Antragsrecht auf Berichtigung,
$# Antragsrecht auf Übertragung ist wegen nicht nötiger Zustimmung zur Verarbeitung bzw. Art und Nutzung der erhobenen Daten nicht gegeben.
$#
$# <h4>Beschwerderecht
$# <p>
$# Nutzer können sich generell bzgl. jeder Verarbeitung oder Weitergabe von
$# persönlichen Daten bei der Aufsichtbehörde beschweren.
$# Im Fall der LMU ist dies der oben genannte bayerische Datenschutzbeauftragte.
$# Ansonsten können auch alle anderen oben genannten Ansprechpartner
$# bzgl. Beschwerden und Nachfragen kontaktiert werden.
$#
$# <h4>Verpflichtung zur Teilnahme an der Verarbeitung
$# <p>
$# Der Nutzer ist bei Nutzung dieses Dienstes verpflichtet die Daten bereitzustellen und
$# verarbeiten zu lassen. Wir behalten uns das Recht vor,
$# Nutzer, die die Daten nicht bereitstellen, von der Nutzung des Dienstes auszuschließen.
$#

View File

@ -0,0 +1,48 @@
function setupDatepicker(wrapper) {
"use strict";
var config = {
dtLocal: {
enableTime: true,
altInput: true,
altFormat: "j. F Y, H:i", // maybe interpolate these formats for locale
dateFormat: "Y-m-dTH:i",
time_24hr: true
},
d: {
altFormat: "j. F Y",
dateFormat: "Y-m-d",
altInput: true
},
t: {
enableTime: true,
noCalendar: true,
altFormat: "H:i",
dateFormat: "H:i",
altInput: true,
time_24hr: true
}
};
Array.from(wrapper.querySelectorAll('input[type="date"]')).forEach(function(el) {
flatpickr(el, config.d);
});
Array.from(wrapper.querySelectorAll('input[type="time"]')).forEach(function(el) {
flatpickr(el, config.t);
});
Array.from(wrapper.querySelectorAll('input[type="datetime-local"]')).forEach(function(el) {
flatpickr(el, config.dtLocal);
});
}
document.addEventListener('DOMContentLoaded', function() {
var I18N = {
filesSelected: 'Dateien ausgewählt', // TODO: interpolate these to be translated
selectFile: 'Datei auswählen',
selectFiles: 'Datei(en) auswählen',
};
window.utils.setup('flatpickr', document.body, { setupFunction: setupDatepicker });
window.utils.setup('showHide', document.body);
window.utils.setup('inputs', document.body, { i18n: I18N });
});

View File

@ -14,7 +14,7 @@
<h4>
neue geplante Features:
<ul>
<li> Stundenplan/Kalender
<li> Stundenplan/Kalender mit Veranstaltungen und Klausuren
<li> Vollständige Vorlesungshomepages
<li> Vollständige Internationalisierung deutsch/englisch/...

View File

@ -0,0 +1,21 @@
document.addEventListener('DOMContentLoaded', function () {
var themeSelector = document.querySelector('#theme-select');
if (themeSelector) {
themeSelector.addEventListener('change', function() {
// get rid of old themes on body
var options = Array.from(themeSelector.options)
.forEach(function (option) {
document.body.classList.remove(optionToTheme(option));
});
// add newly selected theme
document.body.classList.add(optionToTheme(themeSelector.selectedOptions[0]));
});
}
function optionToTheme(option) {
return optionValue = 'theme--' + option.value;
}
});

View File

@ -0,0 +1,96 @@
$newline never
<div .container>
<section>
<h2>Ansprechpartner
<h3>Inhalt
<ul style="list-style-type: none">
<li>Dr Steffen Jost
<li>Akademischer Rat
<li>Oettingenstraße 67
<li>D-80538 München
<li>E-Mail: #
<a href="mailto:jost@tcs.ifi.lmu.de">
jost@tcs.ifi.lmu.de
<li>Web: #
<a href="https://www.tcs.ifi.lmu.de/mitarbeiter/steffen-jost">
https://www.tcs.ifi.lmu.de/mitarbeiter/steffen-jost
<li>Telefon: +49 (0) 89 / 2180 - 9139
<h3>Jugendschutz
<ul style="list-style-type: none">
<li>Robert Hofer
<li>Leiter Rechnerbetriebsgruppe
<li>Oettingenstraße 67
<li>D-80538 München
<li>E-Mail: #
<a href="mailto:rbg@ifi.lmu.de">
rbg@ifi.lmu.de
<li>Web: #
<a href="https://www.rz.ifi.lmu.de/rbg/">
https://www.rz.ifi.lmu.de/rbg/
<li>Telefon: +49 (0) 89 / 2180 - 9198
<div .container>
<section>
<h2>Anschrift
<h3>Rechnerbetriebsgruppe des Department "Institut für Informatik" der Ludwig-Maximilians-Universität München
<ul style="list-style-type: none">
<li>Oettingenstraße 67
<li>D-80538 München
<li>E-Mail: rbg@ifi.lmu.de
<li>Web: https://www.rz.ifi.lmu.de/rbg/
<li>Telefon: +49 (0) 89 / 2180 - 9198
<p>
Die Rechnerbetriebgruppe ist eine Organisation des Department "Institut für Informatik",
welches ein Teil der Ludwig-Maximilians-Universität München ist.
Die Rechnerbetriebsgruppe wird vertreten durch ihren Leiter und unterliegt
der Aufsicht des IT Beauftragen des Vorstands des Departments.
<h3>Department "Institut für Informatik" der Ludwig-Maximilians-Universität München
<ul style="list-style-type: none">
<li>Oettingenstraße 67
<li>D-80538 München
<li>Telefon: +49 (0) 89 / 2180 - 9141
<li>E-Mail: geschaeftsstelle@ifi.lmu.de
<li>Web: https://www.ifi.lmu.de/
<p>
Das Department "Institut für Informatik" ist Teil der Ludwig-Maximilians-Universität
München. Das Department wird durch die Direktorin bzw. den Direktor des Departments vertreten.
<h3>LMU - Ludwig-Maximilians-Universität München
<ul style="list-style-type: none">
<li>Geschwister-Scholl-Platz 1
<li>80539 München<
<li>Telefon: +49 (0) 89 / 2180 - 0
<li>E-Mail: #
<a href="mailto:praesidium@lmu.de">
praesidium@lmu.de
<li>Web: #
<a href="https://www.lmu.de/">
https://www.lmu.de/
<p>
Die Ludwig-Maximilians-Universität München ist eine Körperschaft des
Öffentlichen Rechts. Sie wird durch den Präsidenten der LMU gesetzlich vertreten.
<div .container>
<section>
<h2>Umsatzsteuer-Identifikationsnummer der LMU
<p>
Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz: DE 811205325
<div .container>
<section>
<h2>Zuständige Aufsichtsbehörde
<p>
Bayerisches Staatsministerium für
Bildung und Kultus, Wissenschaft und Kunst
<br>
80327 München

View File

@ -11,7 +11,7 @@ $newline never
}
<body>
<h1>
_{MsgMailSheetInactiveIntro (CI.original courseName) termDesc sheetName}
_{MsgMailSheetInactiveIntro (CI.original courseName) termDesc sheetName nrSubs nrSubmitters}
<p>
<a href=@{CSheetR tid ssh csh shn SShowR}>
#{sheetName}

View File

@ -0,0 +1,35 @@
$newline never
\<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<style>
h1 {
font-size: 1.25em;
font-variant: small-caps;
font-weight: normal;
}
<body>
<h1>
_{MsgMailUserRightsIntro userDisplayName userEmail}
$with numSchools <- length adminSchools
$if numSchools > 0
<p>
<h2>_{MsgAdminFor} _{MsgForSchools numSchools}
<ul>
$forall sn <- adminSchools
<li>#{sn}
$with numSchools <- length lecturerSchools
$if numSchools > 0
<p>
<h2>_{MsgLecturerFor} _{MsgForSchools numSchools}
<ul>
$forall sn <- lecturerSchools
<li>#{sn}
<p>
<a href=@{CourseNewR}>
_{MsgMailLecturerRights numSchools}
$else
<p>_{MsgMailNoLecturerRights}
^{editNotifications}

View File

@ -3,7 +3,7 @@ $forall FileUploadInfo{..} <- fileInfos
<div .file-container :fuiChecked:.file-container--checked>
<label .file-container__label.btn for=#{fuiHtmlId}>#{fuiTitle}
<div .checkbox>
<input .file-container__checkbox.js-file-checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId} type="checkbox">
<input .file-container__checkbox.file-checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId} type="checkbox">
<label for=#{fuiHtmlId}>
$# new files
@ -15,6 +15,6 @@ $# new files
<div .file-input__unpack>
<label for=#{fieldId}_zip>ZIPs automatisch entpacken
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips}>
<div class="js-tooltip">
<div class="tooltip">
<div class="tooltip__handle">
<div class="tooltip__content">Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu.

View File

@ -1,17 +0,0 @@
document.addEventListener('setup', function (e) {
var themeSelector = e.detail.scope.querySelector('#theme-select');
themeSelector.addEventListener('change', function() {
// get rid of old themes on body
var options = Array.from(themeSelector.options)
.forEach(function (option) {
document.body.classList.remove(optionToTheme(option));
});
// add newly selected theme
document.body.classList.add(optionToTheme(themeSelector.selectedOptions[0]));
});
function optionToTheme(option) {
return optionValue = 'theme--' + option.value;
}
});

View File

@ -21,7 +21,7 @@ $maybe descr <- sheetDescription sheet
<dd .deflist__dd>_{sheetSubmissionMode sheet}
$case sheetSubmissionMode sheet
$of CorrectorSubmissions
<div .js-tooltip>
<div .tooltip>
<div .tooltip__handle>
<div .tooltip__content>_{MsgSheetCorrectorSubmissionsTip}
$of _

View File

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

View File

@ -1,43 +0,0 @@
document.addEventListener('setup', function(e) {
"use strict";
if (e.detail.module && e.detail.module !== 'datepicker')
return;
var config = {
dtLocal: {
enableTime: true,
altInput: true,
altFormat: "j. F Y, H:i",
dateFormat: "Y-m-dTH:i",
time_24hr: true
},
d: {
altFormat: "j. F Y",
dateFormat: "Y-m-d",
altInput: true
},
t: {
enableTime: true,
noCalendar: true,
altFormat: "H:i",
dateFormat: "H:i",
altInput: true,
time_24hr: true
}
};
Array.from(e.detail.scope.querySelectorAll('input[type="date"]')).forEach(function(el) {
flatpickr(el, config.d);
});
Array.from(e.detail.scope.querySelectorAll('input[type="time"]')).forEach(function(el) {
flatpickr(el, config.t);
});
Array.from(e.detail.scope.querySelectorAll('input[type="datetime-local"]')).forEach(function(el) {
flatpickr(el, config.dtLocal);
});
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'datepicker' }, bubbles: true, cancelable: true }));
});

View File

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

View File

@ -1,147 +0,0 @@
(function() {
'use strict';
window.utils = window.utils || {};
// allows for multiple file uploads with separate inputs
window.utils.initializeFileUpload = function(input) {
var isMulti = input.hasAttribute('multiple');
var fileList = isMulti ? addFileList() : null;
var label = addFileLabel();
function renderFileList(files) {
fileList.innerHTML = '';
Array.from(files).forEach(function(file, index) {
var fileDisplayEl = document.createElement('li');
fileDisplayEl.innerHTML = file.name;
fileList.appendChild(fileDisplayEl);
});
}
function updateLabel(files) {
if (files.length) {
if (isMulti) {
label.innerText = files.length + ' Dateien ausgwählt';
} else {
label.innerHTML = files[0].name;
}
} else {
resetFileLabel();
}
}
function addFileList() {
var list = document.createElement('ol');
list.classList.add('file-input__list');
var unpackEl = input.parentElement.querySelector('.file-input__unpack');
if (unpackEl) {
input.parentElement.insertBefore(list, unpackEl);
} else {
input.parentElement.appendChild(list);
}
return list;
}
function addFileLabel() {
var label = document.createElement('label');
label.classList.add('file-input__label');
label.setAttribute('for', input.id);
input.parentElement.insertBefore(label, input);
return label;
}
function resetFileLabel() {
// interpolate translated String here
label.innerText = 'Datei' + (isMulti ? 'en' : '') + ' auswählen';
}
// initial setup
resetFileLabel();
input.classList.add('file-input__input--hidden');
input.addEventListener('change', function() {
input.dispatchEvent(new Event('input'));
if (isMulti) {
renderFileList(input.files);
}
updateLabel(input.files);
});
}
// to remove previously uploaded files
window.utils.reactiveFileCheckbox = function(input) {
// adds eventlistener(s)
function addListener(container) {
input.addEventListener('change', function(event) {
container.classList.toggle('file-container--checked', this.checked);
});
}
// initial setup
function setup() {
var cont = input.parentNode;
while (cont !== document.body) {
if (cont.matches('.file-container')) {
break;
}
cont = cont.parentNode;
}
addListener(cont);
}
setup();
}
window.utils.initializeCheckboxRadio = function(input, type) {
if (!input.parentElement.classList.contains(type)) {
var parentEl = input.parentElement;
var siblingEl = input.nextElementSibling;
var wrapperEl = document.createElement('div');
var labelEl = document.createElement('label');
wrapperEl.classList.add(type);
labelEl.setAttribute('for', input.id);
wrapperEl.appendChild(input);
wrapperEl.appendChild(labelEl);
if (siblingEl) {
parentEl.insertBefore(wrapperEl, siblingEl);
} else {
parentEl.appendChild(wrapperEl);
}
}
}
})();
document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'inputs')
return;
// initialize checkboxes
Array.from(e.detail.scope.querySelectorAll('input[type="checkbox"]:not(.js-initialized)')).forEach(function(inp) {
window.utils.initializeCheckboxRadio(inp, 'checkbox');
inp.classList.add("js-initialized");
});
// initialize radios
Array.from(e.detail.scope.querySelectorAll('input[type="radio"]:not(.js-initialized)')).forEach(function(inp) {
window.utils.initializeCheckboxRadio(inp, 'radio');
inp.classList.add("js-initialized");
});
// initialize file-upload-fields
Array.from(e.detail.scope.querySelectorAll('input[type="file"]:not(.js-initialized)')).forEach(function(inp) {
window.utils.initializeFileUpload(inp);
inp.classList.add("js-initialized");
});
// initialize file-checkbox-fields
Array.from(e.detail.scope.querySelectorAll('.js-file-checkbox:not(.js-initialized)')).forEach(function(inp) {
window.utils.reactiveFileCheckbox(inp);
inp.classList.add("js-initialized");
});
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'inputs' }, bubbles: true, cancelable: true }));
});

View File

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

View File

@ -1,58 +0,0 @@
/**
* div
* div.js-show-hide__toggle
* toggle here
* div
* content here
*/
document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'showHide')
return;
var LSNAME = 'SHOW_HIDE';
function addEventHandler(el) {
el.addEventListener('click', function elClickListener() {
var newState = el.parentElement.classList.toggle('js-show-hide--collapsed');
updateLSState(el.dataset.shIndex || null, newState);
});
}
function updateLSState(index, state) {
if (!index) {
return false;
}
var lsData = fromLocalStorage();
lsData[index] = state;
window.localStorage.setItem(LSNAME, JSON.stringify(lsData));
}
function collapsedStateInLocalStorage(index) {
return fromLocalStorage()[index] || null;
}
function fromLocalStorage() {
return JSON.parse(window.localStorage.getItem(LSNAME)) || {};
}
Array
.from(e.detail.scope.querySelectorAll('.js-show-hide__toggle'))
.forEach(function(el) {
var index = el.dataset.shIndex || null;
el.parentElement.classList.toggle(
'js-show-hide--collapsed',
collapsedStateInLocalStorage(index) || el.dataset.collapsed === 'true'
);
Array.from(el.parentElement.children).forEach(function(el) {
if (!el.classList.contains('js-show-hide__toggle')) {
el.classList.add('js-show-hide__target');
}
});
addEventHandler(el);
});
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'showHide' }, bubbles: true, cancelable: true }))
});

View File

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

View File

@ -1,7 +0,0 @@
.tab-opener {
background-color: var(--color-dark);
&.tab-visible {
border-bottom-color: var(--color-primary);
}
}

View File

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

View File

@ -1,67 +0,0 @@
(function() {;
'use strict';
window.utils = window.utils || {};
window.utils.tooltip = function(tt) {
var handle = tt.querySelector('.tooltip__handle');
var content = tt.querySelector('.tooltip__content');
var left = false;
// initially set content to hidden
content.classList.add('hidden');
handle.addEventListener('mouseenter', function() {
left = false;
content.classList.toggle('to-left', handle.getBoundingClientRect().left + 300 > window.innerWidth);
content.classList.remove('hidden');
});
handle.addEventListener('mouseleave', function() {
left = true;
window.setTimeout(function() {
if (left) {
content.classList.add('hidden');
}
}, 250);
});
};
window.utils.tooltipFromAttribute = function(el) {
var tt = document.createElement('div');
var handle = document.createElement('div');
var content = document.createElement('div');
tt.classList.add('js-tooltip');
handle.classList.add('tooltip__handle');
content.classList.add('tooltip__content', 'hidden');
handle.innerText = '?';
content.innerHTML = el.getAttribute('data-tooltip');
tt.appendChild(handle);
tt.appendChild(content);
if (el.nextSiblingElement) {
el.parentElement.insertBefore(tt, el.nextSiblingElement);
} else {
el.parentElement.appendChild(tt);
}
};
})();
document.addEventListener('setup', function(e) {
// JS-TOOLTIPS NOT USED CURRENTLY.
// initialize tooltips set via `data-tooltip`
// Array.from(e.detail.scope.querySelectorAll('[data-tooltip]')).forEach(function(el) {
// window.utils.tooltipFromAttribute(el)
// });
// initialize tooltips
// Array.from(e.detail.scope.querySelectorAll('.js-tooltip')).forEach(function(tt) {
// window.utils.tooltip(tt);
// });
});

View File

@ -1,186 +1,10 @@
(function collonadeClosure() {
'use strict';
window.utils = window.utils || {};
window.utils.asyncTable = function(wrapper, options) {
var tableIdent = wrapper.dataset.dbtIdent;
var shortCircuitHeader = #{String (toPathPiece HeaderDBTableShortcircuit)};
var ths = [];
var pageLinks = [];
var pagesizeForm;
var scrollTable;
function init() {
var table = wrapper.querySelector('#' + tableIdent);
if (!table) {
return;
}
scrollTable = wrapper.querySelector('.scrolltable');
// sortable table headers
ths = Array.from(table.querySelectorAll('th.sortable')).map(function(th) {
return { element: th };
});
// pagination links
var pagination = wrapper.querySelector('#' + tableIdent + '-pagination');
if (pagination) {
pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) {
return { element: link };
});
}
// pagesize form
pagesizeForm = wrapper.querySelector('#' + tableIdent + '-pagesize-form');
// take options into account
if (options && options.scrollTo) {
window.scrollTo(options.scrollTo);
}
if (options && options.horizPos && scrollTable) {
scrollTable.scrollLeft = options.horizPos;
}
setupListeners();
wrapper.classList.add('js-initialized');
}
function setupListeners() {
ths.forEach(function(th) {
th.clickHandler = function(event) {
var boundClickHandler = clickHandler.bind(this);
var horizPos = (scrollTable || {}).scrollLeft;
boundClickHandler(event, { horizPos });
};
th.element.addEventListener('click', th.clickHandler);
});
pageLinks.forEach(function(link) {
link.clickHandler = function(event) {
var boundClickHandler = clickHandler.bind(this);
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]');
pagesizeSelect.addEventListener('change', changePagesizeHandler);
}
}
function removeListeners() {
ths.forEach(function(th) {
th.element.removeEventListener('click', th.clickHandler);
});
pageLinks.forEach(function(link) {
link.element.removeEventListener('click', link.clickHandler);
});
if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]')
pagesizeSelect.removeEventListener('change', changePagesizeHandler);
}
}
function clickHandler(event, options) {
event.preventDefault();
var url = new URL(window.location.origin + window.location.pathname + getClickDestination(this));
updateTableFrom(url, options);
}
function getClickDestination(el) {
if (!el.querySelector('a')) {
return '';
}
return el.querySelector('a').getAttribute('href');
}
function changePagesizeHandler(event) {
var currentTableUrl = wrapper.dataset.currentUrl || window.location.href;
var url = getUrlWithUpdatedPagesize(currentTableUrl, event.target.value);
url = getUrlWithResetPagenumber(url);
updateTableFrom(url);
}
function getUrlWithUpdatedPagesize(url, pagesize) {
if (url.indexOf('pagesize') >= 0) {
return url.replace(/pagesize=(\d+|all)/, 'pagesize=' + pagesize);
} else if (url.indexOf('?') >= 0) {
return url += '&' + tableIdent + '-pagesize=' + pagesize;
}
return url += '?' + tableIdent + '-pagesize=' + pagesize;
}
function getUrlWithResetPagenumber(url) {
return url.replace(/-page=\d+/, '-page=0');
}
function updateWrapperContents(newHtml, options) {
wrapper.innerHTML = newHtml;
wrapper.classList.remove("js-initialized");
// setup the wrapper and its components to behave async again
window.utils.asyncTable(wrapper, options);
// make sure to hide any new submit buttons
document.dispatchEvent(new CustomEvent('setup', {
detail: {
scope: wrapper,
module: 'autoSubmit'
}
}));
}
// fetches new sorted table from url with params and replaces contents of current table
function updateTableFrom(url, options) {
fetch(url, {
credentials: 'same-origin',
headers: {
'Accept': 'text/html',
[shortCircuitHeader]: tableIdent
}
}).then(function(response) {
if (!response.ok) {
throw new Error('Looks like there was a problem fetching ' + url + '. Status Code: ' + response.status);
}
return response.text();
}).then(function(data) {
wrapper.dataset.currentUrl = url;
removeListeners();
updateWrapperContents(data, options);
}).catch(function(err) {
console.error(err);
});
}
init();
};
})();
document.addEventListener('DOMContentLoaded', function() {
var dbtIdent = #{String $ dbtIdent};
var headerDBTableShortcircuit = #{String (toPathPiece HeaderDBTableShortcircuit)};
var selector = '#' + dbtIdent + '-table-wrapper:not(.js-initialized)';
var wrapper = document.querySelector(selector);
if (wrapper) {
wrapper.dataset.dbtIdent = dbtIdent;
window.utils.asyncTable(wrapper);
window.utils.setup('asyncTable', wrapper, { headerDBTableShortcircuit, dbtIdent });
}
});

View File

@ -1,6 +1,4 @@
<div .container>
<h3>
Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
<section>
^{features}
@ -8,13 +6,13 @@
<section>
<h2>
Bekannte Bugs
<h3>
Stand: Februar 2019
<ul>
<li>
Login ist u.U. anders als im alten System, z.B. <span style="font-family:monospace">@campus.lmu.de</span> statt <span style="font-family:monospace">@lmu.de</span>
Login ist u.U. anders als im alten System, z.B. momentan geht nur <span style="font-family:monospace">@campus.lmu.de</span> aber nicht die Abkürzung <span style="font-family:monospace">@lmu.de</span>
<li>
Favicon ist default des Frameworks
<li>
Format von Bewertungsdateien ist provisorisch
Format von Bewertungsdateien ist noch provisorisch
<section>
<h2>
@ -22,26 +20,6 @@
<p #changelog>
#{changeLog}
<section>
<h2>
Impressum
<ul style="list-style-type: none">
<li>
Dr Steffen Jost
<li>
<a href="mailto:jost@tcs.ifi.lmu.de">
jost@tcs.ifi.lmu.de
<li>
Lehr- und Forschungseinheit für
Theoretische Informatik
<li>
Institut für Informatik
<li>
Ludwig-Maximilians-Universität München
<li>
Oettingenstr. 67, 80538 München
<section>
<p #gitrev>
#{gitInfo}

View File

@ -1,8 +1,8 @@
<div #alerts .alerts>
<div #alerts-1 .alerts> <!-- make wIdent work here instead of '#alerts-1' -->
<div .alerts__toggler>
$forall (status, msg) <- mmsgs
$with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}">
<div .alert.alert-#{status2}>
<div .alert__closer>
<div .alert__icon>
<div .alert__content>

View File

@ -1,18 +1,4 @@
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 }))
var alertsElement = document.querySelector('#' + 'alerts-1');
window.utils.setup('alerts', alertsElement);
});

View File

@ -1,23 +0,0 @@
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.aside = function(asideEl) {
function init() {
var favoritesBtn = document.querySelector('.navbar__list-item--favorite');
favoritesBtn.addEventListener('click', function(event) {
favoritesBtn.classList.toggle('navbar__list-item--active');
asideEl.classList.toggle('main__aside--expanded');
event.preventDefault();
}, true);
}
init();
};
})();
document.addEventListener('DOMContentLoaded', function() {
var asidenavEl = document.querySelector('.main__aside');
window.utils.aside(asidenavEl);
});

View File

@ -16,10 +16,11 @@ $newline never
<a .asidenav__link-wrapper href=@{courseRoute}>
<div .asidenav__link-shorthand>#{courseShorthand}
<div .asidenav__link-label>#{courseName}
<ul .asidenav__nested-list.list--iconless>
$forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions
$case menuItemType
$of PageActionPrime
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=#{route}>_{menuItemLabel}
$of _
<div .asidenav__nested-list-wrapper>
<ul .asidenav__nested-list.list--iconless>
$forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions
$case menuItemType
$of PageActionPrime
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=#{route}>_{menuItemLabel}
$of _

View File

@ -0,0 +1,4 @@
document.addEventListener('DOMContentLoaded', function() {
var asidenavEl = document.querySelector('.main__aside');
window.utils.setup('aside', asidenavEl);
});

View File

@ -0,0 +1,5 @@
document.addEventListener('DOMContentLoaded', function() {
Array.from(document.querySelectorAll('form')).forEach(function(form) {
window.utils.setup('form', form);
});
});

View File

@ -51,4 +51,4 @@ $#
\ (_{title $ getSum $ summary ^. _numSheetsPoints})
$# Kurze Alternative mit Hashtag-Symbol für "Anzahl"
$# \ (##{display $ summary ^. _numSheetsPoints})
<td .table__td>#{display $ summary ^. _numSheets}
<td .table__td>#{display $ summary ^. _numSheets}

Some files were not shown because too many files have changed in this diff Show More