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 CourseNewHeading: Neuen Kurs anlegen
CourseEditHeading tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} editieren CourseEditHeading tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} editieren
CourseEditTitle: Kurs editieren/anlegen CourseEditTitle: Kurs editieren/anlegen
CourseMembers: Teilnehmer CourseMember: Teilnehmer
CourseMembersCount num@Int64: #{display num} CourseMembersCount num@Int64: #{display num}
CourseMembersCountLimited num@Int64 max@Int64: #{display num}/#{display max} CourseMembersCountLimited num@Int64 max@Int64: #{display num}/#{display max}
CourseName: Name CourseName: Name
@ -88,6 +88,7 @@ CourseFilterSearch: Volltext-Suche
CourseFilterRegistered: Registriert CourseFilterRegistered: Registriert
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen? CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
CourseDeleted: Kurs gelöscht CourseDeleted: Kurs gelöscht
CourseUserNote: Notiz
NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht. NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht.
NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht. NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht.
@ -235,6 +236,7 @@ ProfileHeading: Benutzereinstellungen
ProfileFor: Benutzereinstellungen für ProfileFor: Benutzereinstellungen für
ProfileDataHeading: Gespeicherte Benutzerdaten ProfileDataHeading: Gespeicherte Benutzerdaten
ImpressumHeading: Impressum ImpressumHeading: Impressum
DataProtHeading: Datenschutzerklärung
SystemMessageHeading: Uni2work Statusmeldung SystemMessageHeading: Uni2work Statusmeldung
SystemMessageListHeading: Uni2work Statusmeldungen SystemMessageListHeading: Uni2work Statusmeldungen
@ -340,6 +342,7 @@ AccessRightsFor: Berechtigungen für
AdminFor: Administrator AdminFor: Administrator
LecturerFor: Dozent LecturerFor: Dozent
LecturersFor: Dozenten LecturersFor: Dozenten
ForSchools n@Int: für #{pluralDE n "Institut" "Institute"}
UserListTitle: Komprehensive Benutzerliste UserListTitle: Komprehensive Benutzerliste
AccessRightsSaved: Berechtigungsänderungen wurden gespeichert. 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 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. 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 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 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. 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 MailEditNotifications: Benachrichtigungen ein-/ausschalten
MailSubjectSupport: Supportanfrage MailSubjectSupport: Supportanfrage
@ -473,6 +482,7 @@ NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr
NotificationTriggerSheetInactive: Abgabefrist eines meiner Übungsblätter ist abgelaufen NotificationTriggerSheetInactive: Abgabefrist eines meiner Übungsblätter ist abgelaufen
NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt
NotificationTriggerCorrectionsNotDistributed: Abgaben eines meiner Übungsblätter konnten keinem Korrektur zugeteilt werden NotificationTriggerCorrectionsNotDistributed: Abgaben eines meiner Übungsblätter konnten keinem Korrektur zugeteilt werden
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
CorrCreate: Abgaben erstellen CorrCreate: Abgaben erstellen
UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}" UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}"
@ -571,6 +581,7 @@ InvalidRoute: Konnte URL nicht interpretieren
MenuHome: Aktuell MenuHome: Aktuell
MenuImpressum: Impressum MenuImpressum: Impressum
MenuDataProt: Datenschutz
MenuVersion: Versionsgeschichte MenuVersion: Versionsgeschichte
MenuHelp: Hilfe MenuHelp: Hilfe
MenuProfile: Anpassen MenuProfile: Anpassen

View File

@ -1,7 +1,7 @@
School json School json
name (CI Text) name (CI Text)
shorthand (CI Text) shorthand (CI Text) -- SchoolKey :: SchoolShorthand -> SchoolId
UniqueSchool name UniqueSchool name
UniqueSchoolShorthand shorthand -- required for Normalisation of CI Text 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 deriving Eq Show Generic

37
routes
View File

@ -10,22 +10,23 @@
-- Admins always have access to entities within their assigned schools. -- Admins always have access to entities within their assigned schools.
-- --
-- Access Tags: -- Access Tags:
-- !free -- free for all -- !free -- free for all
-- !lecturer -- lecturer for this course (or for any school, if route is not connected to a course) -- !lecturer -- lecturer for this course (or for any school, if route is not connected to a course)
-- !corrector -- corrector for this sheet (or the submission, if route is connected to a submission, or the course, if route is not connected to a sheet, or any course, if route is not connected to a course) -- !corrector -- corrector for this sheet (or the submission, if route is connected to a submission, or the course, if route is not connected to a sheet, or any course, if route is not connected to a course)
-- !registered -- participant for this course (no effect outside of courses) -- !registered -- participant for this course (no effect outside of courses)
-- !participant -- connected with a given course (not necessarily registered), i.e. has a submission, is a corrector, etc. (no effect outside of courses) -- !participant -- connected with a given course (not necessarily registered), i.e. has a submission, is a corrector, etc. (no effect outside of courses)
-- !owner -- part of the group of owners of this submission -- !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 -- !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 -- !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) -- !materials -- only if course allows all materials to be free (no meaning outside of courses)
-- !time -- access depends on time somehow -- !time -- access depends on time somehow
-- !read -- only if it is read-only access (i.e. GET but not POST) -- !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) -- !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 -- !no-escalation --
-- !development -- like free, but only for development builds -- !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 /static StaticR EmbeddedStatic appStatic !free
/auth AuthR Auth getAuth !free /auth AuthR Auth getAuth !free
@ -39,9 +40,13 @@
/users/#CryptoUUIDUser/hijack AdminHijackUserR POST !adminANDno-escalation /users/#CryptoUUIDUser/hijack AdminHijackUserR POST !adminANDno-escalation
/admin/test AdminTestR GET POST /admin/test AdminTestR GET POST
/admin/errMsg AdminErrMsgR GET POST /admin/errMsg AdminErrMsgR GET POST
/impressum ImpressumR GET !free
/version VersionR GET !free
/info InfoR 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 HelpR GET POST !free
/help/lecturer InfoLecturerR GET !lecturer /help/lecturer InfoLecturerR GET !lecturer

View File

@ -7,7 +7,7 @@ import Import.NoFoundation
import Database.Persist.Sql (SqlBackendCanRead) import Database.Persist.Sql (SqlBackendCanRead)
import Utils.Form import Utils.Form
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
@ -54,4 +54,4 @@ dummyLogin = AuthPlugin{..}
apDispatch _ _ = notFound apDispatch _ _ = notFound
apLogin toMaster = do apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm (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 | MsgCampusSubmit
| MsgCampusInvalidCredentials | MsgCampusInvalidCredentials
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry] findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
findUser LdapConf{..} ldap campusIdent = Ldap.search ldap ldapBase userSearchSettings userFilter 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.time ldapSearchTimeout
, Ldap.derefAliases Ldap.DerefAlways , Ldap.derefAliases Ldap.DerefAlways
] ]
userPrincipalName :: Ldap.Attr userPrincipalName :: Ldap.Attr
userPrincipalName = Ldap.Attr "userPrincipalName" userPrincipalName = Ldap.Attr "userPrincipalName"
@ -105,7 +105,7 @@ campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
apDispatch _ _ = notFound apDispatch _ _ = notFound
apLogin toMaster = do apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard campusForm (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard campusForm
$(widgetFile "widgets/campus-login-form") $(widgetFile "widgets/campus-login/campus-login-form")
data CampusUserException = CampusUserLdapError LdapPoolError data CampusUserException = CampusUserLdapError LdapPoolError
| CampusUserHostNotResolved String | CampusUserHostNotResolved String

View File

@ -35,7 +35,7 @@ hashForm = HashLogin
<*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing <*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing
<* submitButton <* submitButton
hashLogin :: ( YesodAuth site hashLogin :: ( YesodAuth site
, YesodPersist site , YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site) , SqlBackendCanRead (YesodPersistBackend site)
@ -90,5 +90,5 @@ hashLogin pwHashAlgo = AuthPlugin{..}
apDispatch _ _ = notFound apDispatch _ _ = notFound
apLogin toMaster = do apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm (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 | num == 1 = singularForm
| otherwise = pluralForm | 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 -- Messages creates type UniWorXMessage and RenderMessage UniWorX instance
mkMessage "UniWorX" "messages/uniworx" "de" mkMessage "UniWorX" "messages/uniworx" "de"
mkMessageVariant "UniWorX" "Campus" "messages/campus" "de" mkMessageVariant "UniWorX" "Campus" "messages/campus" "de"
@ -969,19 +982,19 @@ siteLayout' headingOverride widget = do
-- you to use normal widget features in default-layout. -- you to use normal widget features in default-layout.
navbar :: Widget navbar :: Widget
navbar = $(widgetFile "widgets/navbar") navbar = $(widgetFile "widgets/navbar/navbar")
asidenav :: Widget asidenav :: Widget
asidenav = $(widgetFile "widgets/asidenav") asidenav = $(widgetFile "widgets/asidenav/asidenav")
footer :: Widget footer :: Widget
footer = $(widgetFile "widgets/footer") footer = $(widgetFile "widgets/footer/footer")
alerts :: Widget alerts :: Widget
alerts = $(widgetFile "widgets/alerts/alerts") alerts = $(widgetFile "widgets/alerts/alerts")
contentHeadline :: Maybe Widget contentHeadline :: Maybe Widget
contentHeadline = headingOverride <|> (pageHeading =<< mcurrentRoute) contentHeadline = headingOverride <|> (pageHeading =<< mcurrentRoute)
breadcrumbsWgt :: Widget breadcrumbsWgt :: Widget
breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs") breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs/breadcrumbs")
pageaction :: Widget pageaction :: Widget
pageaction = $(widgetFile "widgets/pageaction") pageaction = $(widgetFile "widgets/pageaction/pageaction")
-- functions to determine if there are page-actions (primary or secondary) -- functions to determine if there are page-actions (primary or secondary)
hasPageActions, hasSecondaryPageActions, hasPrimaryPageActions :: Bool hasPageActions, hasSecondaryPageActions, hasPrimaryPageActions :: Bool
hasPageActions = hasPrimaryPageActions || hasSecondaryPageActions hasPageActions = hasPrimaryPageActions || hasSecondaryPageActions
@ -989,25 +1002,35 @@ siteLayout' headingOverride widget = do
hasPrimaryPageActions = any (is _PageActionPrime) $ toListOf (traverse . _1 . _menuItemType) menuTypes hasPrimaryPageActions = any (is _PageActionPrime) $ toListOf (traverse . _1 . _menuItemType) menuTypes
pc <- widgetToPageContent $ do pc <- widgetToPageContent $ do
addScript $ StaticR js_vendor_zepto_js -- 3rd party
addScript $ StaticR js_vendor_flatpickr_js addScript $ StaticR js_vendor_flatpickr_js
addScript $ StaticR js_polyfills_fetchPolyfill_js addScript $ StaticR js_vendor_zepto_js
addScript $ StaticR js_polyfills_urlPolyfill_js
addScript $ StaticR js_utils_featureChecker_js
addScript $ StaticR js_utils_tabber_js
addScript $ StaticR js_utils_alerts_js
addStylesheet $ StaticR css_vendor_flatpickr_css addStylesheet $ StaticR css_vendor_flatpickr_css
addStylesheet $ StaticR css_vendor_fontawesome_css addStylesheet $ StaticR css_vendor_fontawesome_css
-- fonts
addStylesheet $ StaticR css_fonts_css 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_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 "default-layout")
$(widgetFile "standalone/modal") $(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") withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
@ -1038,13 +1061,18 @@ applySystemMessages = liftHandlerT . runDB . runConduit $ selectSource [] [] .|
-- Define breadcrumbs. -- Define breadcrumbs.
instance YesodBreadcrumbs UniWorX where instance YesodBreadcrumbs UniWorX where
breadcrumb (AuthR _) = return ("Login" , Just HomeR) breadcrumb (AuthR _) = return ("Login" , Just HomeR)
breadcrumb HomeR = return ("Uni2work" , Nothing) breadcrumb HomeR = return ("Uni2work" , Nothing)
breadcrumb UsersR = return ("Benutzer" , Just HomeR) breadcrumb UsersR = return ("Benutzer" , Just HomeR)
breadcrumb AdminTestR = return ("Test" , Just HomeR) breadcrumb AdminTestR = return ("Test" , Just HomeR)
breadcrumb (AdminUserR _) = return ("Users" , Just UsersR) breadcrumb (AdminUserR _) = return ("Users" , Just UsersR)
breadcrumb VersionR = return ("Impressum" , Just HomeR)
breadcrumb HelpR = return ("Hilfe" , Just HomeR) 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 InfoLecturerR = return ("Veranstalter" , Just HelpR)
breadcrumb ProfileR = return ("User" , Just HomeR) breadcrumb ProfileR = return ("User" , Just HomeR)
@ -1116,10 +1144,18 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
, return MenuItem
{ menuItemType = Footer
, menuItemLabel = MsgMenuDataProt
, menuItemIcon = Just "shield"
, menuItemRoute = SomeRoute DataProtR
, menuItemModal = False
, menuItemAccessCallback' = return True
}
, return MenuItem , return MenuItem
{ menuItemType = Footer { menuItemType = Footer
, menuItemLabel = MsgMenuImpressum , menuItemLabel = MsgMenuImpressum
, menuItemIcon = Just "book" , menuItemIcon = Just "file-signature"
, menuItemRoute = SomeRoute ImpressumR , menuItemRoute = SomeRoute ImpressumR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
@ -1161,17 +1197,17 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
} }
, return MenuItem , return MenuItem
{ menuItemType = NavbarAside { menuItemType = NavbarAside
, menuItemLabel = MsgMenuCourseList , menuItemLabel = MsgMenuTermShow
, menuItemIcon = Just "graduation-cap" , menuItemIcon = Just "calendar-alt" -- SJ wrote: calendar icon, since Term will be repleaced with TimeTable in the future; arguably Term is more calendar-like than courses anyway!!!
, menuItemRoute = SomeRoute CourseListR , menuItemRoute = SomeRoute TermShowR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
, return MenuItem , return MenuItem
{ menuItemType = NavbarAside { menuItemType = NavbarAside
, menuItemLabel = MsgMenuTermShow , menuItemLabel = MsgMenuCourseList
, 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!!! , menuItemIcon = Just "graduation-cap"
, menuItemRoute = SomeRoute TermShowR , menuItemRoute = SomeRoute CourseListR
, menuItemModal = False , menuItemModal = False
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }

View File

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

View File

@ -1,3 +1,5 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Course where module Handler.Course where
import Import import Import
@ -89,8 +91,8 @@ colRegTo = sortable (Just "register-to") (i18nCell MsgRegisterTo)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
maybe mempty timeCell courseRegisterTo maybe mempty timeCell courseRegisterTo
colParticipants :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colMembers :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colParticipants = sortable (Just "participants") (i18nCell MsgCourseMembers) colMembers = sortable (Just "members") (i18nCell MsgCourseMember)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of $ \DBRow{ dbrOutput=(Entity _ Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of
Nothing -> MsgCourseMembersCount currentParticipants Nothing -> MsgCourseMembersCount currentParticipants
Just limit -> MsgCourseMembersCountLimited currentParticipants limit Just limit -> MsgCourseMembersCountLimited currentParticipants limit
@ -137,7 +139,7 @@ makeCourseTable whereClause colChoices psValidator = do
, ( "schoolshort", SortColumn $ \(_course `E.InnerJoin` school) -> school E.^. SchoolShorthand) , ( "schoolshort", SortColumn $ \(_course `E.InnerJoin` school) -> school E.^. SchoolShorthand)
, ( "register-from", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterFrom) , ( "register-from", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterFrom)
, ( "register-to", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterTo) , ( "register-to", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterTo)
, ( "participants", SortColumn course2Participants ) , ( "members", SortColumn course2Participants )
, ( "registered", SortColumn $ course2Registered muid) , ( "registered", SortColumn $ course2Registered muid)
] ]
, dbtFilter = Map.fromList -- OverloadedLists does not work with the templates here , dbtFilter = Map.fromList -- OverloadedLists does not work with the templates here
@ -221,7 +223,7 @@ getTermSchoolCourseListR tid ssh = do
, colCShortDescr , colCShortDescr
, colRegFrom , colRegFrom
, colRegTo , colRegTo
, colParticipants , colMembers
, maybe mempty (const colRegistered) muid , maybe mempty (const colRegistered) muid
] ]
whereClause (course, _, _) = whereClause (course, _, _) =
@ -245,7 +247,7 @@ getTermCourseListR tid = do
, colSchoolShort , colSchoolShort
, colRegFrom , colRegFrom
, colRegTo , colRegTo
, colParticipants , colMembers
, maybe mempty (const colRegistered) muid , maybe mempty (const colRegistered) muid
] ]
whereClause (course, _, _) = course E.^. CourseTerm E.==. E.val tid 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 (Just _) | not registered -> bimap Just Just <$> mreq textField (fslpI MsgCourseSecret "Code") Nothing
_ -> return (Nothing,Nothing) _ -> return (Nothing,Nothing)
(btnRes, btnView) <- mreq (buttonField $ bool BtnRegister BtnDeregister registered) "buttonField ignores settings anyway" 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 let msecretRes | Just res <- msecretRes' = Just <$> res
| otherwise = FormSuccess Nothing | otherwise = FormSuccess Nothing
return (btnRes *> ((==msecret) <$> msecretRes), widget) -- checks that correct button was pressed, and ignores result of btnRes 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 :: 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 getCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html

View File

@ -183,16 +183,31 @@ homeUser uid = do
$(widgetFile "homeUser") $(widgetFile "homeUser")
-- (widgetFile "dsgvDisclaimer") -- (widgetFile "dsgvDisclaimer")
-- | Versionsgeschichte
getVersionR :: Handler TypedContent getVersionR :: Handler TypedContent
getVersionR = getInfoR -- TODO getVersionR = getInfoR -- TODO
getImpressumR :: Handler TypedContent -- | Impressum
getImpressumR = getInfoR -- TODO 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 :: Handler TypedContent
getInfoR = selectRep $ do 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") let features = $(widgetFile "featureList")
gitInfo :: Text gitInfo :: Text
gitInfo = $gitDescribe <> " (" <> $gitCommitDate <> ")" gitInfo = $gitDescribe <> " (" <> $gitCommitDate <> ")"

View File

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

View File

@ -205,7 +205,7 @@ getSheetListR tid ssh csh = do
mkRoute = do mkRoute = do
cid' <- mkCid cid' <- mkCid
return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR 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 in cellTell acell $ stats submissionRatingPoints
, sortable Nothing -- (Just "percent") , sortable Nothing -- (Just "percent")

View File

@ -1,6 +1,8 @@
module Handler.Users where module Handler.Users where
import Import import Import
import Jobs
-- import Data.Text -- import Data.Text
import Handler.Utils import Handler.Utils
@ -12,15 +14,21 @@ import qualified Data.Set as Set
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
hijackUserForm :: CryptoUUIDUser -> Form () hijackUserForm :: CryptoUUIDUser -> Form ()
hijackUserForm cID csrf = do hijackUserForm cID csrf = do
(uidResult, uidView) <- mforced hiddenField "" (cID :: CryptoUUIDUser) (uidResult, uidView) <- mforced hiddenField "" (cID :: CryptoUUIDUser)
(btnResult, btnView) <- mreq (buttonField BtnHijack) "" Nothing (btnResult, btnView) <- mreq (buttonField BtnHijack) "" Nothing
return (() <$ uidResult <* btnResult, mconcat [toWidget csrf, fvInput uidView, fvInput btnView]) return (() <$ uidResult <* btnResult, mconcat [toWidget csrf, fvInput uidView, fvInput btnView])
-- 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 :: Handler Html
getUsersR = do getUsersR = do
let let
@ -71,28 +79,60 @@ getUsersR = do
psValidator = def psValidator = def
& defaultSorting [SortAscBy "name", SortAscBy "display-name"] & defaultSorting [SortAscBy "name", SortAscBy "display-name"]
((), userList) <- runDB $ dbTable psValidator DBTable ((), userList) <- runDB $ do
{ dbtSQLQuery = return :: E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (Entity User)) schoolOptions <- map (CI.original . schoolName . entityVal &&& CI.original . unSchoolKey . entityKey)
, dbtRowKey = (E.^. UserId) <$> selectList [] [Asc SchoolName]
, dbtColonnade
, dbtProj = return dbTable psValidator DBTable
, dbtSorting = Map.fromList { dbtSQLQuery = return :: E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (Entity User))
[ ( "name" , dbtRowKey = (E.^. UserId)
, SortColumn $ \user -> user E.^. UserSurname , dbtColonnade
) , dbtProj = return
, ( "display-name" , dbtSorting = Map.fromList
, SortColumn $ \user -> user E.^. UserDisplayName [ ( "name"
) , SortColumn $ \user -> user E.^. UserSurname
, ( "matriculation" )
, SortColumn $ \user -> user E.^. UserMatrikelnummer , ( "display-name"
) , SortColumn $ \user -> user E.^. UserDisplayName
] )
, dbtFilter = mempty , ( "matriculation"
, dbtFilterUI = mempty , SortColumn $ \user -> user E.^. UserMatrikelnummer
, dbtStyle = def )
, dbtParams = def ]
, dbtIdent = "users" :: Text , 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 defaultLayout $ do
setTitleI MsgUserListTitle setTitleI MsgUserListTitle
@ -116,7 +156,8 @@ postAdminUserR uuid = do
adminId <- requireAuthId adminId <- requireAuthId
uid <- decrypt uuid uid <- decrypt uuid
let fromSchoolList = Set.fromList . map (userAdminSchool . entityVal) 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 <$> get404 uid
<*> selectList [UserAdminUser ==. adminId] [] <*> selectList [UserAdminUser ==. adminId] []
<*> E.select ( E.from $ \school -> do <*> E.select ( E.from $ \school -> do
@ -132,7 +173,7 @@ postAdminUserR uuid = do
-- above data is needed for both form generation and result evaluation -- above data is needed for both form generation and result evaluation
let userRightsForm :: Form [(SchoolId, Bool, Bool)] let userRightsForm :: Form [(SchoolId, Bool, Bool)]
userRightsForm csrf = do 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 if Set.member sid adminSchools
then do then do
cbAdmin <- mreq checkBoxField "" (Just isAdmin) cbAdmin <- mreq checkBoxField "" (Just isAdmin)
@ -144,7 +185,7 @@ postAdminUserR uuid = do
return (school, cbAdmin, cbLecturer) return (school, cbAdmin, cbLecturer)
let result = forM boxRights $ \(Entity sid _, (resAdmin,_), (resLecturer, _)) -> let result = forM boxRights $ \(Entity sid _, (resAdmin,_), (resLecturer, _)) ->
(,,) <$> pure 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 let userRightsAction changes = do
void . runDB $ void . runDB $
forM changes $ \(sid, userAdmin, userLecturer) -> forM changes $ \(sid, userAdmin, userLecturer) ->
@ -158,6 +199,7 @@ postAdminUserR uuid = do
then void . insertUnique $ UserLecturer uid sid then void . insertUnique $ UserLecturer uid sid
else deleteBy $ UniqueSchoolLecturer uid sid else deleteBy $ UniqueSchoolLecturer uid sid
-- Note: deleteWhere would not work well here since we filter by adminSchools -- 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 addMessageI Info MsgAccessRightsSaved
((result, formWidget),formEnctype) <- runFormPost userRightsForm ((result, formWidget),formEnctype) <- runFormPost userRightsForm
formResult result userRightsAction 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 False@ is an icon that denotes that something™ is not visible
visibleWidget True = mempty visibleWidget True = mempty
visibleWidget False = [whamlet|<i .fas .fa-eye-slash>|] 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 (deleteFormWdgt, deleteFormEnctype) <- generateFormPost $ confirmForm' drRecords confirmString
Just targetRoute <- getCurrentRoute Just targetRoute <- getCurrentRoute
sendResponse =<< 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 Nothing = pointsField
pointsFieldMax (Just maxp) = checkBool (<= maxp) (MsgPointsTooHigh maxp) 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 :: Field Handler TermId
termsActiveField = selectField $ optionsPersistKey [TermActive ==. True] [Desc TermStart] termName termsActiveField = selectField $ optionsPersistKey [TermActive ==. True] [Desc TermStart] termName
@ -588,9 +591,9 @@ multiAction acts defAction = do
widgets <- mapM mToWidget results widgets <- mapM mToWidget results
let actionWidgets = Map.foldrWithKey accWidget [] widgets let actionWidgets = Map.foldrWithKey accWidget [] widgets
accWidget _act Nothing = id 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 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) multiActionA :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action)
=> FieldSettings UniWorX => FieldSettings UniWorX

View File

@ -25,7 +25,7 @@ gradeSummaryWidget title sts =
hasMarkedPasses = positiveSum $ numMarkedPasses sumSummaries hasMarkedPasses = positiveSum $ numMarkedPasses sumSummaries
hasPoints = positiveSum $ numSheetsPoints sumSummaries hasPoints = positiveSum $ numSheetsPoints sumSummaries
hasMarkedPoints = positiveSum $ numMarkedPoints sumSummaries hasMarkedPoints = positiveSum $ numMarkedPoints sumSummaries
rowWdgts = [ $(widgetFile "widgets/gradingSummaryRow") rowWdgts = [ $(widgetFile "widgets/grading-summary/grading-summary-row")
| (sumHeader,summary) <- | (sumHeader,summary) <-
[ (MsgSheetTypeNormal' ,normalSummary) [ (MsgSheetTypeNormal' ,normalSummary)
, (MsgSheetTypeBonus' ,bonusSummary) , (MsgSheetTypeBonus' ,bonusSummary)
@ -33,4 +33,4 @@ gradeSummaryWidget title sts =
] ] ] ]
in if 0 == numSheets sumSummaries in if 0 == numSheets sumSummaries
then mempty 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 :: IsDBTable m a => Text -> Text -> DBCell m a
userCell displayName surname = cell $ nameWidget displayName surname 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: -- Just for documentation purposes; inline this code instead:
maybeTimeCell :: IsDBTable m a => Maybe UTCTime -> DBCell m a maybeTimeCell :: IsDBTable m a => Maybe UTCTime -> DBCell m a
maybeTimeCell = maybe mempty timeCell maybeTimeCell = maybe mempty timeCell
@ -110,3 +122,10 @@ correctorStateCell sc =
correctorLoadCell :: IsDBTable m a => SheetCorrector -> DBCell m a correctorLoadCell :: IsDBTable m a => SheetCorrector -> DBCell m a
correctorLoadCell sc = correctorLoadCell sc =
i18nCell $ sheetCorrectorLoad 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 , let t' = toPathPiece $ SortingSetting t d
] ]
wIdent :: Text -> Text wIdent :: Text -> Text
wIdent = toPathPiece . WithIdent dbtIdent wIdent = toPathPiece . WithIdent dbtIdent
dbsAttrs' dbsAttrs'
| not $ null dbtIdent = ("id", dbtIdent) : dbsAttrs | not $ null dbtIdent = ("id", dbtIdent) : dbsAttrs
| otherwise = dbsAttrs | otherwise = dbsAttrs
@ -802,7 +802,7 @@ cellTooltip :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a -
cellTooltip msg = cellContents.mapped %~ (<> tipWdgt) cellTooltip msg = cellContents.mapped %~ (<> tipWdgt)
where where
tipWdgt = [whamlet| tipWdgt = [whamlet|
<div .js-tooltip> <div .tooltip>
<div .tooltip__handle> <div .tooltip__handle>
<div .tooltip__content>_{msg} <div .tooltip__content>_{msg}
|] |]

View File

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

View File

@ -2,7 +2,9 @@ module Jobs.Handler.QueueNotification
( dispatchJobQueueNotification ( dispatchJobQueueNotification
) where ) where
import Import import Import hiding ((\\))
import Data.List ((\\))
import Jobs.Types import Jobs.Types
@ -20,7 +22,7 @@ dispatchJobQueueNotification jNotification = runDBJobs . setSerializable $ do
guard $ notificationAllowed userNotificationSettings nClass guard $ notificationAllowed userNotificationSettings nClass
return uid return uid
determineNotificationCandidates :: Notification -> DB [Entity User] determineNotificationCandidates :: Notification -> DB [Entity User]
determineNotificationCandidates NotificationSubmissionRated{..} determineNotificationCandidates NotificationSubmissionRated{..}
= E.select . E.from $ \(user `E.InnerJoin` submissionUser) -> do = E.select . E.from $ \(user `E.InnerJoin` submissionUser) -> do
@ -40,20 +42,34 @@ determineNotificationCandidates NotificationSheetSoonInactive{..}
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
return user return user
determineNotificationCandidates NotificationSheetInactive{..} determineNotificationCandidates NotificationSheetInactive{..}
= E.select . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` sheet) -> do = 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.^. LecturerCourse E.==. sheet E.^. SheetCourse
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
return user return user
determineNotificationCandidates NotificationCorrectionsAssigned{..} determineNotificationCandidates NotificationCorrectionsAssigned{..}
= selectList [UserId ==. nUser] [] = selectList [UserId ==. nUser] []
determineNotificationCandidates NotificationCorrectionsNotDistributed{nSheet} determineNotificationCandidates NotificationCorrectionsNotDistributed{nSheet}
= E.select . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` sheet) -> do = 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.^. LecturerCourse E.==. sheet E.^. SheetCourse
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
return user 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 :: Notification -> DB NotificationTrigger
classifyNotification NotificationSubmissionRated{..} = do classifyNotification NotificationSubmissionRated{..} = do
Sheet{sheetType} <- belongsToJust submissionSheet =<< getJust nSubmission Sheet{sheetType} <- belongsToJust submissionSheet =<< getJust nSubmission
@ -65,5 +81,6 @@ classifyNotification NotificationSheetSoonInactive{} = return NTSheetSoonInactiv
classifyNotification NotificationSheetInactive{} = return NTSheetInactive classifyNotification NotificationSheetInactive{} = return NTSheetInactive
classifyNotification NotificationCorrectionsAssigned{} = return NTCorrectionsAssigned classifyNotification NotificationCorrectionsAssigned{} = return NTCorrectionsAssigned
classifyNotification NotificationCorrectionsNotDistributed{} = return NTCorrectionsNotDistributed 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.SheetInactive
import Jobs.Handler.SendNotification.CorrectionsAssigned import Jobs.Handler.SendNotification.CorrectionsAssigned
import Jobs.Handler.SendNotification.CorrectionsNotDistributed import Jobs.Handler.SendNotification.CorrectionsNotDistributed
import Jobs.Handler.SendNotification.UserRightsUpdate
dispatchJobSendNotification :: UserId -> Notification -> Handler () dispatchJobSendNotification :: UserId -> Notification -> Handler ()

View File

@ -12,6 +12,8 @@ import Handler.Utils.Mail
import Text.Hamlet import Text.Hamlet
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Database.Esqueleto as E
dispatchNotificationSheetSoonInactive :: SheetId -> UserId -> Handler () dispatchNotificationSheetSoonInactive :: SheetId -> UserId -> Handler ()
dispatchNotificationSheetSoonInactive nSheet jRecipient = userMailT jRecipient $ do dispatchNotificationSheetSoonInactive nSheet jRecipient = userMailT jRecipient $ do
(Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do (Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do
@ -33,10 +35,16 @@ dispatchNotificationSheetSoonInactive nSheet jRecipient = userMailT jRecipient $
dispatchNotificationSheetInactive :: SheetId -> UserId -> Handler () dispatchNotificationSheetInactive :: SheetId -> UserId -> Handler ()
dispatchNotificationSheetInactive nSheet jRecipient = userMailT jRecipient $ do dispatchNotificationSheetInactive nSheet jRecipient = userMailT jRecipient $ do
(Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do (Course{..}, Sheet{..}, nrSubs, nrSubmitters) <- liftHandlerT . runDB $ do
sheet <- getJust nSheet sheet <- getJust nSheet
course <- belongsToJust sheetCourse sheet 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 setSubjectI $ MsgMailSubjectSheetInactive courseShorthand sheetName
MsgRenderer mr <- getMailMsgRenderer MsgRenderer mr <- getMailMsgRenderer
@ -49,4 +57,4 @@ dispatchNotificationSheetInactive nSheet jRecipient = userMailT jRecipient $ do
addAlternatives $ do addAlternatives $ do
let editNotifications = $(ihamletFile "templates/mail/editNotifications.hamlet") let editNotifications = $(ihamletFile "templates/mail/editNotifications.hamlet")
providePreferredAlternative ($(ihamletFile "templates/mail/sheetInactive.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX)) 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 | JobHelpRequest { jSender :: Either (Maybe Address) UserId
, jRequestTime :: UTCTime , jRequestTime :: UTCTime
, jHelpRequest :: Text, jReferer :: Maybe Text } , jHelpRequest :: Text, jReferer :: Maybe Text }
| JobSetLogSettings { jInstance :: InstanceId, jLogSettings :: LogSettings } | JobSetLogSettings { jInstance :: InstanceId, jLogSettings :: LogSettings }
| JobDistributeCorrections { jSheet :: SheetId } | JobDistributeCorrections { jSheet :: SheetId }
deriving (Eq, Ord, Show, Read, Generic, Typeable) deriving (Eq, Ord, Show, Read, Generic, Typeable)
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId } data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
@ -27,6 +27,7 @@ data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
| NotificationSheetInactive { nSheet :: SheetId } | NotificationSheetInactive { nSheet :: SheetId }
| NotificationCorrectionsAssigned { nUser :: UserId, nSheet :: SheetId } | NotificationCorrectionsAssigned { nUser :: UserId, nSheet :: SheetId }
| NotificationCorrectionsNotDistributed { 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) deriving (Eq, Ord, Show, Read, Generic, Typeable)
instance Hashable Job instance Hashable Job

View File

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

View File

@ -10,15 +10,12 @@ import qualified Data.Set as Set
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
-- import Database.Persist -- currently not needed here -- import Database.Persist -- currently not needed here
-- ezero = E.val (0 :: Int64)
emptyOrIn :: PersistField typ => emptyOrIn :: PersistField typ =>
E.SqlExpr (E.Value typ) -> Set typ -> E.SqlExpr (E.Value Bool) E.SqlExpr (E.Value typ) -> Set typ -> E.SqlExpr (E.Value Bool)
emptyOrIn criterion testSet emptyOrIn criterion testSet
| Set.null testSet = E.val True | Set.null testSet = E.val True
| otherwise = criterion `E.in_` E.valList (Set.toList testSet) | otherwise = criterion `E.in_` E.valList (Set.toList testSet)
entities2map :: PersistEntity record => [Entity record] -> Map (Key record) record entities2map :: PersistEntity record => [Entity record] -> Map (Key record) record
entities2map = foldl' (\m entity -> Map.insert (entityKey entity) (entityVal entity) m) Map.empty 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 :: Monad m => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do renderAForm formLayout aform fragment = do
(res, ($ []) -> fieldViews) <- aFormToForm aform (res, ($ []) -> fieldViews) <- aFormToForm aform
let widget = $(widgetFile "widgets/form") let widget = $(widgetFile "widgets/form/form")
return (res, widget) return (res, widget)
-- | special id to identify form section headers, see 'aformSection' and 'formSection' -- | 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 isSel n = (==) (either (const $ map optionInternalValue olOptions) id val !! pred n) . optionInternalValue
nums = map (id &&& withNum theId) [1..length olOptions] nums = map (id &&& withNum theId) [1..length olOptions]
withNum t n = tshow n <> "." <> t withNum t n = tshow n <> "." <> t
$(widgetFile "widgets/permutation") $(widgetFile "widgets/permutation/permutation")
optionsFinite :: ( MonadHandler m optionsFinite :: ( MonadHandler m
, Finite a , Finite a

View File

@ -3,7 +3,7 @@ module Utils.Lens ( module Utils.Lens ) where
import Import.NoFoundation import Import.NoFoundation
import Control.Lens as Utils.Lens hiding ((<.>)) import Control.Lens as Utils.Lens hiding ((<.>))
import Control.Lens.Extras as Utils.Lens (is) 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(..)) 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 _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 makeLenses_ ''SheetCorrector

View File

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

View File

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

View File

@ -17,7 +17,7 @@
text-align: center; text-align: center;
padding: 0 13px; padding: 0 13px;
margin: 0 2px; margin: 0 2px;
background-color: #b3b7c1; background-color: var(--color-dark);
color: white; color: white;
font-size: 16px; font-size: 16px;
text-transform: uppercase; text-transform: uppercase;
@ -35,5 +35,5 @@
.tab-opener.tab-visible { .tab-opener.tab-visible {
background-color: transparent; background-color: transparent;
color: rgb(52, 48, 58); 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; position: relative;
display: inline-block; display: inline-block;
height: 1.5rem; height: 1.5rem;
@ -67,7 +67,7 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.js-tooltip { .tooltip {
display: block; display: block;
margin-top: 10px; margin-top: 10px;

View File

@ -3,6 +3,7 @@
window.utils = window.utils || {}; window.utils = window.utils || {};
var ALERTS_CLASS = 'alerts';
var ALERTS_TOGGLER_CLASS = 'alerts__toggler'; var ALERTS_TOGGLER_CLASS = 'alerts__toggler';
var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible'; var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible';
var ALERTS_TOGGLER_APPEAR_DELAY = 120; var ALERTS_TOGGLER_APPEAR_DELAY = 120;
@ -18,7 +19,11 @@
window.utils.alerts = function(alertsEl) { window.utils.alerts = function(alertsEl) {
if (alertsEl.classList.contains(JS_INITIALIZED_CLASS)) { 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; 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 || {}; 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 // registers input-listener for each element in <inputs> (array) and
// enables <button> if <validation> for these inputs returns true // enables <button> if <formValidator> for these inputs returns true
window.utils.reactiveButton = function(form, button, validation) { window.utils.reactiveButton = function(form, options) {
options = options || {};
var button = options.button;
var requireds = Array.from(form.querySelectorAll('[required]')); var requireds = Array.from(form.querySelectorAll('[required]'));
if (!button) {
throw new Error('Please provide both a button to reactiveButton');
}
if (requireds.length == 0) { if (requireds.length == 0) {
return false; return false;
} }
if (typeof button.dataset.formnorequired !== 'undefined' && button.dataset.formnorequired !== null) { if (typeof button.dataset.formnorequired !== 'undefined' && button.dataset.formnorequired !== null) {
button.addEventListener('click', function() { button.addEventListener('click', function() {
form.submit(); form.submit();
@ -27,7 +71,7 @@
}); });
function updateButtonState() { function updateButtonState() {
if (validation.call(null, requireds) === true) { if (formValidator(requireds) === true) {
button.removeAttribute('disabled'); button.removeAttribute('disabled');
} else { } else {
button.setAttribute('disabled', 'true'); 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) { var fields = fieldSets.map(function(fs) {
return { return {
fieldSet: fs, fieldSet: fs,
@ -64,58 +115,11 @@
updateFields(); 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> <h4>
neue geplante Features: neue geplante Features:
<ul> <ul>
<li> Stundenplan/Kalender <li> Stundenplan/Kalender mit Veranstaltungen und Klausuren
<li> Vollständige Vorlesungshomepages <li> Vollständige Vorlesungshomepages
<li> Vollständige Internationalisierung deutsch/englisch/... <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> <body>
<h1> <h1>
_{MsgMailSheetInactiveIntro (CI.original courseName) termDesc sheetName} _{MsgMailSheetInactiveIntro (CI.original courseName) termDesc sheetName nrSubs nrSubmitters}
<p> <p>
<a href=@{CSheetR tid ssh csh shn SShowR}> <a href=@{CSheetR tid ssh csh shn SShowR}>
#{sheetName} #{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> <div .file-container :fuiChecked:.file-container--checked>
<label .file-container__label.btn for=#{fuiHtmlId}>#{fuiTitle} <label .file-container__label.btn for=#{fuiHtmlId}>#{fuiTitle}
<div .checkbox> <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}> <label for=#{fuiHtmlId}>
$# new files $# new files
@ -15,6 +15,6 @@ $# new files
<div .file-input__unpack> <div .file-input__unpack>
<label for=#{fieldId}_zip>ZIPs automatisch entpacken <label for=#{fieldId}_zip>ZIPs automatisch entpacken
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips}> <input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips}>
<div class="js-tooltip"> <div class="tooltip">
<div class="tooltip__handle"> <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. <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} <dd .deflist__dd>_{sheetSubmissionMode sheet}
$case sheetSubmissionMode sheet $case sheetSubmissionMode sheet
$of CorrectorSubmissions $of CorrectorSubmissions
<div .js-tooltip> <div .tooltip>
<div .tooltip__handle> <div .tooltip__handle>
<div .tooltip__content>_{MsgSheetCorrectorSubmissionsTip} <div .tooltip__content>_{MsgSheetCorrectorSubmissionsTip}
$of _ $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() { document.addEventListener('DOMContentLoaded', function() {
var dbtIdent = #{String $ dbtIdent}; var dbtIdent = #{String $ dbtIdent};
var headerDBTableShortcircuit = #{String (toPathPiece HeaderDBTableShortcircuit)};
var selector = '#' + dbtIdent + '-table-wrapper:not(.js-initialized)'; var selector = '#' + dbtIdent + '-table-wrapper:not(.js-initialized)';
var wrapper = document.querySelector(selector); var wrapper = document.querySelector(selector);
if (wrapper) { if (wrapper) {
wrapper.dataset.dbtIdent = dbtIdent; window.utils.setup('asyncTable', wrapper, { headerDBTableShortcircuit, dbtIdent });
window.utils.asyncTable(wrapper);
} }
}); });

View File

@ -1,6 +1,4 @@
<div .container> <div .container>
<h3>
Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
<section> <section>
^{features} ^{features}
@ -8,13 +6,13 @@
<section> <section>
<h2> <h2>
Bekannte Bugs Bekannte Bugs
<h3>
Stand: Februar 2019
<ul> <ul>
<li> <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> <li>
Favicon ist default des Frameworks Format von Bewertungsdateien ist noch provisorisch
<li>
Format von Bewertungsdateien ist provisorisch
<section> <section>
<h2> <h2>
@ -22,26 +20,6 @@
<p #changelog> <p #changelog>
#{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> <section>
<p #gitrev> <p #gitrev>
#{gitInfo} #{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> <div .alerts__toggler>
$forall (status, msg) <- mmsgs $forall (status, msg) <- mmsgs
$with status2 <- bool status "info" (status == "") $with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}"> <div .alert.alert-#{status2}>
<div .alert__closer> <div .alert__closer>
<div .alert__icon> <div .alert__icon>
<div .alert__content> <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.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}> <a .asidenav__link-wrapper href=@{courseRoute}>
<div .asidenav__link-shorthand>#{courseShorthand} <div .asidenav__link-shorthand>#{courseShorthand}
<div .asidenav__link-label>#{courseName} <div .asidenav__link-label>#{courseName}
<ul .asidenav__nested-list.list--iconless> <div .asidenav__nested-list-wrapper>
$forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions <ul .asidenav__nested-list.list--iconless>
$case menuItemType $forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions
$of PageActionPrime $case menuItemType
<li .asidenav__nested-list-item> $of PageActionPrime
<a .asidenav__link-wrapper href=#{route}>_{menuItemLabel} <li .asidenav__nested-list-item>
$of _ <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}) \ (_{title $ getSum $ summary ^. _numSheetsPoints})
$# Kurze Alternative mit Hashtag-Symbol für "Anzahl" $# Kurze Alternative mit Hashtag-Symbol für "Anzahl"
$# \ (##{display $ summary ^. _numSheetsPoints}) $# \ (##{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