Merge and by accident info page
This commit is contained in:
commit
10237c4031
@ -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
|
||||||
|
|||||||
@ -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
37
routes
@ -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
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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")
|
||||||
|
|
||||||
|
|||||||
45
src/Database/Esqueleto/Utils.hs
Normal file
45
src/Database/Esqueleto/Utils.hs
Normal 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
|
||||||
|
|
||||||
|
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 <> ")"
|
||||||
|
|||||||
@ -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")
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
31
src/Handler/Utils/Database.hs
Normal file
31
src/Handler/Utils/Database.hs
Normal 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
|
||||||
@ -92,7 +92,7 @@ getDeleteR DeleteRoute{..} = do
|
|||||||
Just targetRoute <- getCurrentRoute
|
Just targetRoute <- getCurrentRoute
|
||||||
|
|
||||||
sendResponse =<<
|
sendResponse =<<
|
||||||
defaultLayout $(widgetFile "widgets/delete-confirmation")
|
defaultLayout $(widgetFile "widgets/delete-confirmation/delete-confirmation")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}
|
||||||
|]
|
|]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
@ -53,6 +55,20 @@ determineNotificationCandidates NotificationCorrectionsNotDistributed{nSheet}
|
|||||||
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
|
||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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 ()
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
27
src/Jobs/Handler/SendNotification/UserRightsUpdate.hs
Normal file
27
src/Jobs/Handler/SendNotification/UserRightsUpdate.hs
Normal 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))
|
||||||
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
@ -1,3 +1,8 @@
|
|||||||
|
# Datei Index
|
||||||
|
|
||||||
|
Database,Esqueleto.*
|
||||||
|
: Hilfsdefinitionen, welche Esqueleto anbieten könnte
|
||||||
|
|
||||||
Utils, Utils.*
|
Utils, Utils.*
|
||||||
: Hilfsfunktionionen _unabhängig von Foundation_
|
: Hilfsfunktionionen _unabhängig von Foundation_
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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;
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
@ -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;
|
||||||
|
|||||||
59
static/js/utils/asidenav.js
Normal file
59
static/js/utils/asidenav.js
Normal 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();
|
||||||
|
};
|
||||||
|
})();
|
||||||
202
static/js/utils/asyncTable.js
Normal file
202
static/js/utils/asyncTable.js
Normal 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();
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -1,4 +0,0 @@
|
|||||||
window.addEventListener('touchstart', function onFirstTouch() {
|
|
||||||
document.body.classList.add('touch-supported');
|
|
||||||
window.removeEventListener('touchstart', onFirstTouch, false);
|
|
||||||
}, false);
|
|
||||||
@ -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
171
static/js/utils/inputs.js
Normal 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
58
static/js/utils/setup.js
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
55
static/js/utils/showHide.js
Normal file
55
static/js/utils/showHide.js
Normal 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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
194
templates/data-protection-de.hamlet
Normal file
194
templates/data-protection-de.hamlet
Normal 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.
|
||||||
|
$#
|
||||||
48
templates/default-layout.julius
Normal file
48
templates/default-layout.julius
Normal 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 });
|
||||||
|
});
|
||||||
@ -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/...
|
||||||
|
|
||||||
|
|||||||
21
templates/formPageI18n.julius
Normal file
21
templates/formPageI18n.julius
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
96
templates/impressum.hamlet
Normal file
96
templates/impressum.hamlet
Normal 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
|
||||||
@ -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}
|
||||||
|
|||||||
35
templates/mail/userRightsUpdate.hamlet
Normal file
35
templates/mail/userRightsUpdate.hamlet
Normal 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}
|
||||||
@ -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.
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -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 _
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
<!-- only here to be able to include datepicker using `toWidget` -->
|
|
||||||
@ -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 }));
|
|
||||||
});
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<!-- only here to be able to include inputs using `toWidget` -->
|
|
||||||
@ -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 }));
|
|
||||||
});
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<!-- only here to be able to include showHide using `toWidget` -->
|
|
||||||
@ -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 }))
|
|
||||||
});
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<!-- only here to be able to include tabber using `toWidget` -->
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
.tab-opener {
|
|
||||||
background-color: var(--color-dark);
|
|
||||||
|
|
||||||
&.tab-visible {
|
|
||||||
border-bottom-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<!-- only here to be able to include tooltips using `toWidget` -->
|
|
||||||
@ -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);
|
|
||||||
// });
|
|
||||||
});
|
|
||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
|
||||||
});
|
|
||||||
@ -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 _
|
||||||
4
templates/widgets/asidenav/asidenav.julius
Normal file
4
templates/widgets/asidenav/asidenav.julius
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var asidenavEl = document.querySelector('.main__aside');
|
||||||
|
window.utils.setup('aside', asidenavEl);
|
||||||
|
});
|
||||||
5
templates/widgets/form/form.julius
Normal file
5
templates/widgets/form/form.julius
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
Array.from(document.querySelectorAll('form')).forEach(function(form) {
|
||||||
|
window.utils.setup('form', form);
|
||||||
|
});
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user