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
|
||||
CourseEditHeading tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} editieren
|
||||
CourseEditTitle: Kurs editieren/anlegen
|
||||
CourseMembers: Teilnehmer
|
||||
CourseMember: Teilnehmer
|
||||
CourseMembersCount num@Int64: #{display num}
|
||||
CourseMembersCountLimited num@Int64 max@Int64: #{display num}/#{display max}
|
||||
CourseName: Name
|
||||
@ -88,6 +88,7 @@ CourseFilterSearch: Volltext-Suche
|
||||
CourseFilterRegistered: Registriert
|
||||
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
|
||||
CourseDeleted: Kurs gelöscht
|
||||
CourseUserNote: Notiz
|
||||
|
||||
NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht.
|
||||
NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht.
|
||||
@ -235,6 +236,7 @@ ProfileHeading: Benutzereinstellungen
|
||||
ProfileFor: Benutzereinstellungen für
|
||||
ProfileDataHeading: Gespeicherte Benutzerdaten
|
||||
ImpressumHeading: Impressum
|
||||
DataProtHeading: Datenschutzerklärung
|
||||
SystemMessageHeading: Uni2work Statusmeldung
|
||||
SystemMessageListHeading: Uni2work Statusmeldungen
|
||||
|
||||
@ -340,6 +342,7 @@ AccessRightsFor: Berechtigungen für
|
||||
AdminFor: Administrator
|
||||
LecturerFor: Dozent
|
||||
LecturersFor: Dozenten
|
||||
ForSchools n@Int: für #{pluralDE n "Institut" "Institute"}
|
||||
UserListTitle: Komprehensive Benutzerliste
|
||||
AccessRightsSaved: Berechtigungsänderungen wurden gespeichert.
|
||||
|
||||
@ -420,11 +423,17 @@ MailSubmissionsUnassignedIntro n@Int courseName@Text termDesc@Text sheetName@She
|
||||
MailSubjectSheetSoonInactive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} kann nur noch kurze Zeit abgegeben werden
|
||||
MailSheetSoonInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) endet in Kürze.
|
||||
MailSubjectSheetInactive csh@CourseShorthand sheetName@SheetName: Abgabfrist für #{sheetName} in #{csh} abgelaufen
|
||||
MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Die Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet.
|
||||
MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName n@Int num@Int64: Die Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet. Es gab #{noneOneMoreDE n "Keine Abgaben" "Nur eine Abgabe von " (display n <> " Abgaben von ")}#{noneOneMoreDE num "" "einem Teilnehmer" (display num <> " Teilnehmern")}.
|
||||
|
||||
MailSubjectCorrectionsAssigned csh@CourseShorthand sheetName@SheetName: Ihnen wurden Korrekturen zu #{sheetName} in #{csh} zugeteilt
|
||||
MailCorrectionsAssignedIntro courseName@Text termDesc@Text sheetName@SheetName n@Int: #{display n} #{pluralDE n "Abgabe wurde" "Abgaben wurden"} Ihnen zur Korrektur für #{sheetName} im Kurs #{courseName} (#{termDesc}) zugeteilt.
|
||||
|
||||
MailSubjectUserRightsUpdate name@Text: Berechtigungen für #{name} aktualisiert
|
||||
MailUserRightsIntro name@Text email@UserEmail: #{name} <#{email}> hat folgende Uni2work Berechtigungen:
|
||||
MailNoLecturerRights: Sie haben derzeit keine Dozenten-Rechte.
|
||||
MailLecturerRights n@Int: Als Dozent dürfen Sie Veranstaltungen innerhalb #{pluralDE n "Ihres Instituts" "Ihrer Institute"} anlegen.
|
||||
|
||||
|
||||
MailEditNotifications: Benachrichtigungen ein-/ausschalten
|
||||
MailSubjectSupport: Supportanfrage
|
||||
|
||||
@ -473,6 +482,7 @@ NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr
|
||||
NotificationTriggerSheetInactive: Abgabefrist eines meiner Übungsblätter ist abgelaufen
|
||||
NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt
|
||||
NotificationTriggerCorrectionsNotDistributed: Abgaben eines meiner Übungsblätter konnten keinem Korrektur zugeteilt werden
|
||||
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
|
||||
|
||||
CorrCreate: Abgaben erstellen
|
||||
UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}"
|
||||
@ -571,6 +581,7 @@ InvalidRoute: Konnte URL nicht interpretieren
|
||||
|
||||
MenuHome: Aktuell
|
||||
MenuImpressum: Impressum
|
||||
MenuDataProt: Datenschutz
|
||||
MenuVersion: Versionsgeschichte
|
||||
MenuHelp: Hilfe
|
||||
MenuProfile: Anpassen
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
School json
|
||||
name (CI Text)
|
||||
shorthand (CI Text)
|
||||
shorthand (CI Text) -- SchoolKey :: SchoolShorthand -> SchoolId
|
||||
UniqueSchool name
|
||||
UniqueSchoolShorthand shorthand -- required for Normalisation of CI Text
|
||||
Primary shorthand -- newtype Key School = SchoolKey { unSchoolKey :: SchoolShorthand }
|
||||
Primary shorthand -- newtype Key School = SchoolKey { unSchoolKey :: SchoolShorthand }
|
||||
deriving Eq Show Generic
|
||||
|
||||
37
routes
37
routes
@ -10,22 +10,23 @@
|
||||
-- Admins always have access to entities within their assigned schools.
|
||||
--
|
||||
-- Access Tags:
|
||||
-- !free -- free for all
|
||||
-- !lecturer -- lecturer for this course (or for any school, if route is not connected to a course)
|
||||
-- !corrector -- corrector for this sheet (or the submission, if route is connected to a submission, or the course, if route is not connected to a sheet, or any course, if route is not connected to a course)
|
||||
-- !registered -- participant for this course (no effect outside of courses)
|
||||
-- !participant -- connected with a given course (not necessarily registered), i.e. has a submission, is a corrector, etc. (no effect outside of courses)
|
||||
-- !owner -- part of the group of owners of this submission
|
||||
-- !capacity -- course this route is associated with has at least one unit of participant capacity
|
||||
-- !empty -- course this route is associated with has no participants whatsoever
|
||||
-- !free -- free for all
|
||||
-- !lecturer -- lecturer for this course (or for any school, if route is not connected to a course)
|
||||
-- !corrector -- corrector for this sheet (or the submission, if route is connected to a submission, or the course, if route is not connected to a sheet, or any course, if route is not connected to a course)
|
||||
-- !registered -- participant for this course (no effect outside of courses)
|
||||
-- !participant -- connected with a given course (not necessarily registered), i.e. has a submission, is a corrector, etc. (no effect outside of courses)
|
||||
-- !owner -- part of the group of owners of this submission
|
||||
-- !capacity -- course this route is associated with has at least one unit of participant capacity
|
||||
-- !empty -- course this route is associated with has no participants whatsoever
|
||||
--
|
||||
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
|
||||
-- !time -- access depends on time somehow
|
||||
-- !read -- only if it is read-only access (i.e. GET but not POST)
|
||||
-- !write -- only if it is write access (i.e. POST only, included for completeness)
|
||||
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
|
||||
-- !time -- access depends on time somehow
|
||||
-- !read -- only if it is read-only access (i.e. GET but not POST)
|
||||
-- !write -- only if it is write access (i.e. POST only, included for completeness)
|
||||
--
|
||||
-- !deprecated -- like free, but logs and gives a warning; entirely disabled in production
|
||||
-- !development -- like free, but only for development builds
|
||||
-- !no-escalation --
|
||||
-- !deprecated -- like free, but logs and gives a warning; entirely disabled in production
|
||||
-- !development -- like free, but only for development builds
|
||||
|
||||
/static StaticR EmbeddedStatic appStatic !free
|
||||
/auth AuthR Auth getAuth !free
|
||||
@ -39,9 +40,13 @@
|
||||
/users/#CryptoUUIDUser/hijack AdminHijackUserR POST !adminANDno-escalation
|
||||
/admin/test AdminTestR GET POST
|
||||
/admin/errMsg AdminErrMsgR GET POST
|
||||
/impressum ImpressumR GET !free
|
||||
/version VersionR GET !free
|
||||
|
||||
|
||||
/info InfoR GET !free
|
||||
/impressum ImpressumR GET !free
|
||||
/info/data DataProtR GET !free
|
||||
/version VersionR GET !free
|
||||
|
||||
|
||||
/help HelpR GET POST !free
|
||||
/help/lecturer InfoLecturerR GET !lecturer
|
||||
|
||||
@ -7,7 +7,7 @@ import Import.NoFoundation
|
||||
import Database.Persist.Sql (SqlBackendCanRead)
|
||||
|
||||
import Utils.Form
|
||||
|
||||
|
||||
import Data.CaseInsensitive (CI)
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
|
||||
@ -54,4 +54,4 @@ dummyLogin = AuthPlugin{..}
|
||||
apDispatch _ _ = notFound
|
||||
apLogin toMaster = do
|
||||
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm
|
||||
$(widgetFile "widgets/dummy-login-form")
|
||||
$(widgetFile "widgets/dummy-login-form/dummy-login-form")
|
||||
|
||||
@ -36,7 +36,7 @@ data CampusMessage = MsgCampusIdentNote
|
||||
| MsgCampusSubmit
|
||||
| MsgCampusInvalidCredentials
|
||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||
|
||||
|
||||
|
||||
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
|
||||
findUser LdapConf{..} ldap campusIdent = Ldap.search ldap ldapBase userSearchSettings userFilter
|
||||
@ -48,7 +48,7 @@ findUser LdapConf{..} ldap campusIdent = Ldap.search ldap ldapBase userSearchSet
|
||||
, Ldap.time ldapSearchTimeout
|
||||
, Ldap.derefAliases Ldap.DerefAlways
|
||||
]
|
||||
|
||||
|
||||
userPrincipalName :: Ldap.Attr
|
||||
userPrincipalName = Ldap.Attr "userPrincipalName"
|
||||
|
||||
@ -105,7 +105,7 @@ campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
|
||||
apDispatch _ _ = notFound
|
||||
apLogin toMaster = do
|
||||
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard campusForm
|
||||
$(widgetFile "widgets/campus-login-form")
|
||||
$(widgetFile "widgets/campus-login/campus-login-form")
|
||||
|
||||
data CampusUserException = CampusUserLdapError LdapPoolError
|
||||
| CampusUserHostNotResolved String
|
||||
|
||||
@ -35,7 +35,7 @@ hashForm = HashLogin
|
||||
<*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing
|
||||
<* submitButton
|
||||
|
||||
|
||||
|
||||
hashLogin :: ( YesodAuth site
|
||||
, YesodPersist site
|
||||
, SqlBackendCanRead (YesodPersistBackend site)
|
||||
@ -90,5 +90,5 @@ hashLogin pwHashAlgo = AuthPlugin{..}
|
||||
apDispatch _ _ = notFound
|
||||
apLogin toMaster = do
|
||||
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm
|
||||
$(widgetFile "widgets/hash-login-form")
|
||||
$(widgetFile "widgets/hash-login-form/hash-login-form")
|
||||
|
||||
|
||||
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
|
||||
| otherwise = pluralForm
|
||||
|
||||
noneOneMoreDE :: (Eq a, Num a)
|
||||
=> a -- ^ Count
|
||||
-> Text -- ^ None
|
||||
-> Text -- ^ Singular
|
||||
-> Text -- ^ Plural
|
||||
-> Text
|
||||
noneOneMoreDE num noneText singularForm pluralForm
|
||||
| num == 0 = noneText
|
||||
| num == 1 = singularForm
|
||||
| otherwise = pluralForm
|
||||
|
||||
|
||||
|
||||
-- Messages creates type UniWorXMessage and RenderMessage UniWorX instance
|
||||
mkMessage "UniWorX" "messages/uniworx" "de"
|
||||
mkMessageVariant "UniWorX" "Campus" "messages/campus" "de"
|
||||
@ -969,19 +982,19 @@ siteLayout' headingOverride widget = do
|
||||
-- you to use normal widget features in default-layout.
|
||||
|
||||
navbar :: Widget
|
||||
navbar = $(widgetFile "widgets/navbar")
|
||||
navbar = $(widgetFile "widgets/navbar/navbar")
|
||||
asidenav :: Widget
|
||||
asidenav = $(widgetFile "widgets/asidenav")
|
||||
asidenav = $(widgetFile "widgets/asidenav/asidenav")
|
||||
footer :: Widget
|
||||
footer = $(widgetFile "widgets/footer")
|
||||
footer = $(widgetFile "widgets/footer/footer")
|
||||
alerts :: Widget
|
||||
alerts = $(widgetFile "widgets/alerts/alerts")
|
||||
contentHeadline :: Maybe Widget
|
||||
contentHeadline = headingOverride <|> (pageHeading =<< mcurrentRoute)
|
||||
breadcrumbsWgt :: Widget
|
||||
breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs")
|
||||
breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs/breadcrumbs")
|
||||
pageaction :: Widget
|
||||
pageaction = $(widgetFile "widgets/pageaction")
|
||||
pageaction = $(widgetFile "widgets/pageaction/pageaction")
|
||||
-- functions to determine if there are page-actions (primary or secondary)
|
||||
hasPageActions, hasSecondaryPageActions, hasPrimaryPageActions :: Bool
|
||||
hasPageActions = hasPrimaryPageActions || hasSecondaryPageActions
|
||||
@ -989,25 +1002,35 @@ siteLayout' headingOverride widget = do
|
||||
hasPrimaryPageActions = any (is _PageActionPrime) $ toListOf (traverse . _1 . _menuItemType) menuTypes
|
||||
|
||||
pc <- widgetToPageContent $ do
|
||||
addScript $ StaticR js_vendor_zepto_js
|
||||
-- 3rd party
|
||||
addScript $ StaticR js_vendor_flatpickr_js
|
||||
addScript $ StaticR js_polyfills_fetchPolyfill_js
|
||||
addScript $ StaticR js_polyfills_urlPolyfill_js
|
||||
addScript $ StaticR js_utils_featureChecker_js
|
||||
addScript $ StaticR js_utils_tabber_js
|
||||
addScript $ StaticR js_utils_alerts_js
|
||||
addScript $ StaticR js_vendor_zepto_js
|
||||
addStylesheet $ StaticR css_vendor_flatpickr_css
|
||||
addStylesheet $ StaticR css_vendor_fontawesome_css
|
||||
-- fonts
|
||||
addStylesheet $ StaticR css_fonts_css
|
||||
addStylesheet $ StaticR css_utils_tabber_css
|
||||
-- polyfills
|
||||
addScript $ StaticR js_polyfills_fetchPolyfill_js
|
||||
addScript $ StaticR js_polyfills_urlPolyfill_js
|
||||
-- JavaScript utils
|
||||
addScript $ StaticR js_utils_alerts_js
|
||||
addScript $ StaticR js_utils_asidenav_js
|
||||
addScript $ StaticR js_utils_asyncTable_js
|
||||
addScript $ StaticR js_utils_form_js
|
||||
addScript $ StaticR js_utils_inputs_js
|
||||
addScript $ StaticR js_utils_setup_js
|
||||
addScript $ StaticR js_utils_showHide_js
|
||||
addScript $ StaticR js_utils_tabber_js
|
||||
addStylesheet $ StaticR css_utils_alerts_scss
|
||||
addStylesheet $ StaticR css_utils_asidenav_scss
|
||||
addStylesheet $ StaticR css_utils_form_scss
|
||||
addStylesheet $ StaticR css_utils_inputs_scss
|
||||
addStylesheet $ StaticR css_utils_showHide_scss
|
||||
addStylesheet $ StaticR css_utils_tabber_scss
|
||||
addStylesheet $ StaticR css_utils_tooltip_scss
|
||||
-- widgets
|
||||
$(widgetFile "default-layout")
|
||||
$(widgetFile "standalone/modal")
|
||||
$(widgetFile "standalone/showHide")
|
||||
$(widgetFile "standalone/inputs")
|
||||
$(widgetFile "standalone/tooltip")
|
||||
$(widgetFile "standalone/tabber")
|
||||
$(widgetFile "standalone/datepicker")
|
||||
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
|
||||
|
||||
|
||||
@ -1038,13 +1061,18 @@ applySystemMessages = liftHandlerT . runDB . runConduit $ selectSource [] [] .|
|
||||
|
||||
-- Define breadcrumbs.
|
||||
instance YesodBreadcrumbs UniWorX where
|
||||
breadcrumb (AuthR _) = return ("Login" , Just HomeR)
|
||||
breadcrumb HomeR = return ("Uni2work" , Nothing)
|
||||
breadcrumb UsersR = return ("Benutzer" , Just HomeR)
|
||||
breadcrumb AdminTestR = return ("Test" , Just HomeR)
|
||||
breadcrumb (AdminUserR _) = return ("Users" , Just UsersR)
|
||||
breadcrumb VersionR = return ("Impressum" , Just HomeR)
|
||||
breadcrumb HelpR = return ("Hilfe" , Just HomeR)
|
||||
breadcrumb (AuthR _) = return ("Login" , Just HomeR)
|
||||
breadcrumb HomeR = return ("Uni2work" , Nothing)
|
||||
breadcrumb UsersR = return ("Benutzer" , Just HomeR)
|
||||
breadcrumb AdminTestR = return ("Test" , Just HomeR)
|
||||
breadcrumb (AdminUserR _) = return ("Users" , Just UsersR)
|
||||
|
||||
breadcrumb InfoR = return ("Information" , Nothing)
|
||||
breadcrumb ImpressumR = return ("Impressum" , Just InfoR)
|
||||
breadcrumb DataProtR = return ("Datenschutz" , Just InfoR)
|
||||
breadcrumb VersionR = return ("Impressum" , Just InfoR)
|
||||
|
||||
breadcrumb HelpR = return ("Hilfe" , Just HomeR)
|
||||
breadcrumb InfoLecturerR = return ("Veranstalter" , Just HelpR)
|
||||
|
||||
breadcrumb ProfileR = return ("User" , Just HomeR)
|
||||
@ -1116,10 +1144,18 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
|
||||
, menuItemModal = False
|
||||
, menuItemAccessCallback' = return True
|
||||
}
|
||||
, return MenuItem
|
||||
{ menuItemType = Footer
|
||||
, menuItemLabel = MsgMenuDataProt
|
||||
, menuItemIcon = Just "shield"
|
||||
, menuItemRoute = SomeRoute DataProtR
|
||||
, menuItemModal = False
|
||||
, menuItemAccessCallback' = return True
|
||||
}
|
||||
, return MenuItem
|
||||
{ menuItemType = Footer
|
||||
, menuItemLabel = MsgMenuImpressum
|
||||
, menuItemIcon = Just "book"
|
||||
, menuItemIcon = Just "file-signature"
|
||||
, menuItemRoute = SomeRoute ImpressumR
|
||||
, menuItemModal = False
|
||||
, menuItemAccessCallback' = return True
|
||||
@ -1161,17 +1197,17 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
|
||||
}
|
||||
, return MenuItem
|
||||
{ menuItemType = NavbarAside
|
||||
, menuItemLabel = MsgMenuCourseList
|
||||
, menuItemIcon = Just "graduation-cap"
|
||||
, menuItemRoute = SomeRoute CourseListR
|
||||
, menuItemLabel = MsgMenuTermShow
|
||||
, menuItemIcon = Just "calendar-alt" -- SJ wrote: calendar icon, since Term will be repleaced with TimeTable in the future; arguably Term is more calendar-like than courses anyway!!!
|
||||
, menuItemRoute = SomeRoute TermShowR
|
||||
, menuItemModal = False
|
||||
, menuItemAccessCallback' = return True
|
||||
}
|
||||
, return MenuItem
|
||||
{ menuItemType = NavbarAside
|
||||
, menuItemLabel = MsgMenuTermShow
|
||||
, menuItemIcon = Just "calendar-alt" -- SJ wrote: calendar icon, since Term will be repleaced with TimeTable in the future; arguably Term is more calendar-like than courses anyway!!!
|
||||
, menuItemRoute = SomeRoute TermShowR
|
||||
, menuItemLabel = MsgMenuCourseList
|
||||
, menuItemIcon = Just "graduation-cap"
|
||||
, menuItemRoute = SomeRoute CourseListR
|
||||
, menuItemModal = False
|
||||
, menuItemAccessCallback' = return True
|
||||
}
|
||||
|
||||
@ -162,7 +162,7 @@ colRating = sortable (Just "rating") (i18nCell MsgRating) $ \DBRow{ dbrOutput=(E
|
||||
cid <- encrypt subId
|
||||
return $ CSubmissionR tid ssh csh sheetName cid CorrectionR
|
||||
in mconcat
|
||||
[ anchorCellM mkRoute $(widgetFile "widgets/rating")
|
||||
[ anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
|
||||
, writerCell $ do
|
||||
let
|
||||
summary :: SheetTypeSummary
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Handler.Course where
|
||||
|
||||
import Import
|
||||
@ -89,8 +91,8 @@ colRegTo = sortable (Just "register-to") (i18nCell MsgRegisterTo)
|
||||
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
|
||||
maybe mempty timeCell courseRegisterTo
|
||||
|
||||
colParticipants :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
|
||||
colParticipants = sortable (Just "participants") (i18nCell MsgCourseMembers)
|
||||
colMembers :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
|
||||
colMembers = sortable (Just "members") (i18nCell MsgCourseMember)
|
||||
$ \DBRow{ dbrOutput=(Entity _ Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of
|
||||
Nothing -> MsgCourseMembersCount currentParticipants
|
||||
Just limit -> MsgCourseMembersCountLimited currentParticipants limit
|
||||
@ -137,7 +139,7 @@ makeCourseTable whereClause colChoices psValidator = do
|
||||
, ( "schoolshort", SortColumn $ \(_course `E.InnerJoin` school) -> school E.^. SchoolShorthand)
|
||||
, ( "register-from", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterFrom)
|
||||
, ( "register-to", SortColumn $ \(course `E.InnerJoin` _school) -> course E.^. CourseRegisterTo)
|
||||
, ( "participants", SortColumn course2Participants )
|
||||
, ( "members", SortColumn course2Participants )
|
||||
, ( "registered", SortColumn $ course2Registered muid)
|
||||
]
|
||||
, dbtFilter = Map.fromList -- OverloadedLists does not work with the templates here
|
||||
@ -221,7 +223,7 @@ getTermSchoolCourseListR tid ssh = do
|
||||
, colCShortDescr
|
||||
, colRegFrom
|
||||
, colRegTo
|
||||
, colParticipants
|
||||
, colMembers
|
||||
, maybe mempty (const colRegistered) muid
|
||||
]
|
||||
whereClause (course, _, _) =
|
||||
@ -245,7 +247,7 @@ getTermCourseListR tid = do
|
||||
, colSchoolShort
|
||||
, colRegFrom
|
||||
, colRegTo
|
||||
, colParticipants
|
||||
, colMembers
|
||||
, maybe mempty (const colRegistered) muid
|
||||
]
|
||||
whereClause (course, _, _) = course E.^. CourseTerm E.==. E.val tid
|
||||
@ -295,7 +297,7 @@ registerForm registered msecret extra = do
|
||||
(Just _) | not registered -> bimap Just Just <$> mreq textField (fslpI MsgCourseSecret "Code") Nothing
|
||||
_ -> return (Nothing,Nothing)
|
||||
(btnRes, btnView) <- mreq (buttonField $ bool BtnRegister BtnDeregister registered) "buttonField ignores settings anyway" Nothing
|
||||
let widget = $(widgetFile "widgets/registerForm")
|
||||
let widget = $(widgetFile "widgets/register-form/register-form")
|
||||
let msecretRes | Just res <- msecretRes' = Just <$> res
|
||||
| otherwise = FormSuccess Nothing
|
||||
return (btnRes *> ((==msecret) <$> msecretRes), widget) -- checks that correct button was pressed, and ignores result of btnRes
|
||||
@ -617,8 +619,81 @@ validateCourse CourseForm{..} =
|
||||
|
||||
|
||||
|
||||
-- CourseUserTable
|
||||
|
||||
type UserTableExpr = (E.SqlExpr (Entity User) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant)) `E.LeftOuterJoin` E.SqlExpr (Entity CourseUserNote)
|
||||
type UserTableWhere = UserTableExpr -> E.SqlExpr (E.Value Bool)
|
||||
type UserTableData = DBRow (Entity User, Entity CourseParticipant, Maybe (Key CourseUserNote))
|
||||
|
||||
userTableQuery :: UserTableWhere -> (UserTableExpr -> v) -> UserTableExpr -> E.SqlQuery v
|
||||
userTableQuery whereClause returnStatement t@((user `E.InnerJoin` participant) `E.LeftOuterJoin` note) = do
|
||||
E.on $ participant E.^. CourseParticipantUser E.==. note E.^. CourseUserNoteUser
|
||||
E.on $ participant E.^. CourseParticipantUser E.==. user E.^. UserId
|
||||
E.where_ $ whereClause t
|
||||
return $ returnStatement t
|
||||
|
||||
instance HasUser UserTableData where
|
||||
hasUser = _entityVal
|
||||
|
||||
instance HasEntity UserTableData User where
|
||||
hasEntity = _dbrOutput . _1
|
||||
|
||||
-- -- there can be only one -- FunctionalDependency violation
|
||||
-- instance HasEntity UserTableData CourseParticipant where
|
||||
-- hasEntity = _dbrOutput . _2
|
||||
|
||||
courseIs :: CourseId -> UserTableWhere
|
||||
courseIs cid ((_user `E.InnerJoin` participant) `E.LeftOuterJoin` _note) = participant E.^. CourseParticipantCourse E.==. E.val cid
|
||||
|
||||
-- TODO: delete commented function
|
||||
-- colUserParticipant' :: IsDBTable m a => Colonnade _ UserTableData (DBCell m a)
|
||||
-- colUserParticipant' = sortable (Just "participant") (i18nCell MsgCourseMember)
|
||||
-- $ \DBRow { dbrOutput=(Entity _ user,_,_) } -> userCell (userDisplayName user) (userSurname user)
|
||||
|
||||
colUserParticipant :: IsDBTable m a => Colonnade _ UserTableData (DBCell m a)
|
||||
colUserParticipant = sortable (Just "participant") (i18nCell MsgCourseMember) cellHasUser
|
||||
|
||||
colUserParticipantLink :: IsDBTable m a => TermId -> SchoolId -> CourseShorthand -> Colonnade _ UserTableData (DBCell m a)
|
||||
colUserParticipantLink tid ssh csh = sortable (Just "participant") (i18nCell MsgCourseMember) (cellHasUserLink courseLink)
|
||||
where
|
||||
courseLink = CourseR tid ssh csh . CUserR
|
||||
|
||||
colUserMatriclenr :: IsDBTable m a => Colonnade _ UserTableData (DBCell m a)
|
||||
colUserMatriclenr = sortable (Just "matriclenumber") (i18nCell MsgMatrikelNr) cellHasMatrikelnummer
|
||||
|
||||
colUserComment :: IsDBTable m a => TermId -> SchoolId -> CourseShorthand -> Colonnade _ UserTableData (DBCell m a)
|
||||
colUserComment tid ssh csh =
|
||||
sortable (Just "course-user-note") (i18nCell MsgCourseUserNote)
|
||||
$ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey) } ->
|
||||
maybeEmpty mbNoteKey $ const $
|
||||
anchorCellM (courseLink <$> encrypt uid) (commentWidget True)
|
||||
where
|
||||
courseLink = CourseR tid ssh csh . CUserR
|
||||
|
||||
makeUserTable :: UserTableWhere -> _ -> _ -> DB Widget
|
||||
makeUserTable _whereClause _colChoices _psValidator =
|
||||
-- do
|
||||
-- dbTable psValidator DBTable
|
||||
-- { userTableQUery whereClause
|
||||
-- ,
|
||||
return [whamlet| Course user table not yet implemented |]
|
||||
|
||||
|
||||
getCUsersR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||
getCUsersR = error "CUsersR: Not implemented"
|
||||
getCUsersR tid ssh csh = do
|
||||
Entity _cid course <- runDB $ getBy404 $ TermSchoolCourseShort tid ssh csh
|
||||
let heading = [whamlet|_{MsgCourseMember} #{courseName course} #{display tid}|]
|
||||
-- whereClause = courseIs cid
|
||||
-- colChoices = [colUserParticipant,colUserMatriclenr,colUserComment tid ssh csh]
|
||||
-- psValidator = def
|
||||
-- tableWidget <- runDB $ makeUserTable whereClause colChoices psValidator
|
||||
siteLayout heading $ do
|
||||
setTitle [shamlet| #{toPathPiece tid} - #{csh}|]
|
||||
[whamlet|
|
||||
User table not yet implemented
|
||||
$# ^{tableWidget}
|
||||
|]
|
||||
|
||||
|
||||
|
||||
getCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html
|
||||
|
||||
@ -183,16 +183,31 @@ homeUser uid = do
|
||||
$(widgetFile "homeUser")
|
||||
-- (widgetFile "dsgvDisclaimer")
|
||||
|
||||
|
||||
-- | Versionsgeschichte
|
||||
getVersionR :: Handler TypedContent
|
||||
getVersionR = getInfoR -- TODO
|
||||
|
||||
getImpressumR :: Handler TypedContent
|
||||
getImpressumR = getInfoR -- TODO
|
||||
-- | Impressum
|
||||
getImpressumR :: Handler Html
|
||||
getImpressumR = -- do
|
||||
siteLayoutMsg' MsgMenuImpressum $ do
|
||||
setTitleI MsgImpressumHeading
|
||||
$(widgetFile "impressum")
|
||||
|
||||
|
||||
-- | Hinweise zu Datenschutz und Aufbewahrungspflichten
|
||||
getDataProtR :: Handler Html
|
||||
getDataProtR = -- do
|
||||
siteLayoutMsg' MsgMenuDataProt $ do
|
||||
setTitleI MsgDataProtHeading
|
||||
$(widgetFile "data-protection-de")
|
||||
|
||||
|
||||
-- | Allgemeine Informationen
|
||||
getInfoR :: Handler TypedContent
|
||||
getInfoR = selectRep $ do
|
||||
provideRep . defaultLayout $ do
|
||||
let infoHeading = [whamlet|Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>|]
|
||||
provideRep . siteLayout infoHeading $ do
|
||||
let features = $(widgetFile "featureList")
|
||||
gitInfo :: Text
|
||||
gitInfo = $gitDescribe <> " (" <> $gitCommitDate <> ")"
|
||||
|
||||
@ -251,7 +251,7 @@ getProfileDataR = do
|
||||
-- Delete Button
|
||||
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form ButtonDelete)
|
||||
defaultLayout $ do
|
||||
let delWdgt = $(widgetFile "widgets/data-delete")
|
||||
let delWdgt = $(widgetFile "widgets/data-delete/data-delete")
|
||||
$(widgetFile "profileData")
|
||||
$(widgetFile "dsgvDisclaimer")
|
||||
|
||||
|
||||
@ -205,7 +205,7 @@ getSheetListR tid ssh csh = do
|
||||
mkRoute = do
|
||||
cid' <- mkCid
|
||||
return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR
|
||||
acell = anchorCellM mkRoute $(widgetFile "widgets/rating")
|
||||
acell = anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
|
||||
in cellTell acell $ stats submissionRatingPoints
|
||||
|
||||
, sortable Nothing -- (Just "percent")
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
module Handler.Users where
|
||||
|
||||
import Import
|
||||
|
||||
import Jobs
|
||||
-- import Data.Text
|
||||
import Handler.Utils
|
||||
|
||||
@ -12,15 +14,21 @@ import qualified Data.Set as Set
|
||||
import qualified Data.Map as Map
|
||||
|
||||
import qualified Database.Esqueleto as E
|
||||
import qualified Database.Esqueleto.Utils as E
|
||||
|
||||
|
||||
hijackUserForm :: CryptoUUIDUser -> Form ()
|
||||
hijackUserForm cID csrf = do
|
||||
(uidResult, uidView) <- mforced hiddenField "" (cID :: CryptoUUIDUser)
|
||||
(btnResult, btnView) <- mreq (buttonField BtnHijack) "" Nothing
|
||||
|
||||
return (() <$ uidResult <* btnResult, mconcat [toWidget csrf, fvInput uidView, fvInput btnView])
|
||||
|
||||
-- In case of refactoring, use this:
|
||||
-- instance HasEntity (DBRow (Entity User)) User where
|
||||
-- hasEntity = _dbrOutput
|
||||
-- instance HasUser (DBRow (Entity USer)) where
|
||||
-- hasUser = _entityVal
|
||||
|
||||
getUsersR :: Handler Html
|
||||
getUsersR = do
|
||||
let
|
||||
@ -71,28 +79,60 @@ getUsersR = do
|
||||
psValidator = def
|
||||
& defaultSorting [SortAscBy "name", SortAscBy "display-name"]
|
||||
|
||||
((), userList) <- runDB $ dbTable psValidator DBTable
|
||||
{ dbtSQLQuery = return :: E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (Entity User))
|
||||
, dbtRowKey = (E.^. UserId)
|
||||
, dbtColonnade
|
||||
, dbtProj = return
|
||||
, dbtSorting = Map.fromList
|
||||
[ ( "name"
|
||||
, SortColumn $ \user -> user E.^. UserSurname
|
||||
)
|
||||
, ( "display-name"
|
||||
, SortColumn $ \user -> user E.^. UserDisplayName
|
||||
)
|
||||
, ( "matriculation"
|
||||
, SortColumn $ \user -> user E.^. UserMatrikelnummer
|
||||
)
|
||||
]
|
||||
, dbtFilter = mempty
|
||||
, dbtFilterUI = mempty
|
||||
, dbtStyle = def
|
||||
, dbtParams = def
|
||||
, dbtIdent = "users" :: Text
|
||||
}
|
||||
((), userList) <- runDB $ do
|
||||
schoolOptions <- map (CI.original . schoolName . entityVal &&& CI.original . unSchoolKey . entityKey)
|
||||
<$> selectList [] [Asc SchoolName]
|
||||
|
||||
dbTable psValidator DBTable
|
||||
{ dbtSQLQuery = return :: E.SqlExpr (Entity User) -> E.SqlQuery (E.SqlExpr (Entity User))
|
||||
, dbtRowKey = (E.^. UserId)
|
||||
, dbtColonnade
|
||||
, dbtProj = return
|
||||
, dbtSorting = Map.fromList
|
||||
[ ( "name"
|
||||
, SortColumn $ \user -> user E.^. UserSurname
|
||||
)
|
||||
, ( "display-name"
|
||||
, SortColumn $ \user -> user E.^. UserDisplayName
|
||||
)
|
||||
, ( "matriculation"
|
||||
, SortColumn $ \user -> user E.^. UserMatrikelnummer
|
||||
)
|
||||
]
|
||||
, dbtFilter = Map.fromList -- OverloadedLists does not work with the templates
|
||||
[ ( "user-search", FilterColumn $ \user criterion ->
|
||||
if Set.null criterion then E.true else -- TODO: why is this condition not needed?
|
||||
-- Set.foldr (\needle acc -> acc E.||. (user E.^. UserDisplayName) `E.hasInfix` needle) eFalse (criterion :: Set.Set Text)
|
||||
E.any (user E.^. UserDisplayName `E.hasInfix`) criterion
|
||||
)
|
||||
, ( "matriculation", FilterColumn $ \user (criterion :: Set.Set Text) -> if
|
||||
| Set.null criterion -> E.true -- TODO: why can this be eFalse and work still?
|
||||
| otherwise -> E.any (user E.^. UserMatrikelnummer `E.hasInfix`) criterion
|
||||
)
|
||||
, ( "school", FilterColumn $ \user criterion -> if
|
||||
| Set.null criterion -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||
| otherwise -> let schools = E.valList (Set.toList criterion) in
|
||||
E.exists ( E.from $ \ulectr -> do
|
||||
E.where_ $ ulectr E.^. UserLecturerUser E.==. user E.^. UserId
|
||||
E.where_ $ ulectr E.^. UserLecturerSchool `E.in_` schools
|
||||
) E.||.
|
||||
E.exists ( E.from $ \uadmin -> do
|
||||
E.where_ $ uadmin E.^. UserAdminUser E.==. user E.^. UserId
|
||||
E.where_ $ uadmin E.^. UserAdminSchool `E.in_` schools
|
||||
)
|
||||
)
|
||||
]
|
||||
, dbtFilterUI = \mPrev -> mconcat
|
||||
[ prismAForm (singletonFilter "user-search") mPrev $ aopt (searchField True) (fslI MsgName)
|
||||
-- , prismAForm (singletonFilter "matriculation" ) mPrev $ aopt (searchField False) (fslI MsgMatrikelNr)
|
||||
, prismAForm (singletonFilter "matriculation" ) mPrev $ aopt matriculationField (fslI MsgMatrikelNr)
|
||||
|
||||
, prismAForm (singletonFilter "school" ) mPrev $ aopt (lift `hoistField` selectFieldList schoolOptions) (fslI MsgCourseSchool)
|
||||
]
|
||||
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||
, dbtParams = def
|
||||
, dbtIdent = "users" :: Text
|
||||
}
|
||||
|
||||
defaultLayout $ do
|
||||
setTitleI MsgUserListTitle
|
||||
@ -116,7 +156,8 @@ postAdminUserR uuid = do
|
||||
adminId <- requireAuthId
|
||||
uid <- decrypt uuid
|
||||
let fromSchoolList = Set.fromList . map (userAdminSchool . entityVal)
|
||||
(User{..}, fromSchoolList -> adminSchools, userRights) <- runDB $ (,,)
|
||||
let unValueRights (school, E.Value isAdmin, E.Value isLecturer) = (school,isAdmin,isLecturer)
|
||||
(User{..}, fromSchoolList -> adminSchools, fmap unValueRights -> userRights) <- runDB $ (,,)
|
||||
<$> get404 uid
|
||||
<*> selectList [UserAdminUser ==. adminId] []
|
||||
<*> E.select ( E.from $ \school -> do
|
||||
@ -132,7 +173,7 @@ postAdminUserR uuid = do
|
||||
-- above data is needed for both form generation and result evaluation
|
||||
let userRightsForm :: Form [(SchoolId, Bool, Bool)]
|
||||
userRightsForm csrf = do
|
||||
boxRights <- forM userRights $ \(school@(Entity sid _), E.Value isAdmin, E.Value isLecturer) ->
|
||||
boxRights <- forM userRights $ \(school@(Entity sid _), isAdmin, isLecturer) ->
|
||||
if Set.member sid adminSchools
|
||||
then do
|
||||
cbAdmin <- mreq checkBoxField "" (Just isAdmin)
|
||||
@ -144,7 +185,7 @@ postAdminUserR uuid = do
|
||||
return (school, cbAdmin, cbLecturer)
|
||||
let result = forM boxRights $ \(Entity sid _, (resAdmin,_), (resLecturer, _)) ->
|
||||
(,,) <$> pure sid <*> resAdmin <*> resLecturer
|
||||
return (result,$(widgetFile "widgets/user-rights-form"))
|
||||
return (result,$(widgetFile "widgets/user-rights-form/user-rights-form"))
|
||||
let userRightsAction changes = do
|
||||
void . runDB $
|
||||
forM changes $ \(sid, userAdmin, userLecturer) ->
|
||||
@ -158,6 +199,7 @@ postAdminUserR uuid = do
|
||||
then void . insertUnique $ UserLecturer uid sid
|
||||
else deleteBy $ UniqueSchoolLecturer uid sid
|
||||
-- Note: deleteWhere would not work well here since we filter by adminSchools
|
||||
queueJob' . JobQueueNotification $ NotificationUserRightsUpdate uid (over _1 (schoolShorthand . entityVal) <$> userRights) -- original rights to check for difference
|
||||
addMessageI Info MsgAccessRightsSaved
|
||||
((result, formWidget),formEnctype) <- runFormPost userRightsForm
|
||||
formResult result userRightsAction
|
||||
|
||||
@ -74,3 +74,8 @@ visibleWidget :: Bool -> Widget
|
||||
-- ^ @visibleWidget False@ is an icon that denotes that something™ is not visible
|
||||
visibleWidget True = mempty
|
||||
visibleWidget False = [whamlet|<i .fas .fa-eye-slash>|]
|
||||
|
||||
commentWidget :: Bool -> Widget
|
||||
-- ^ @commentWidget True@ is an icon that denotes that something™ has a comment
|
||||
commentWidget True = [whamlet|<i .fas .fa-comment-alt>|]
|
||||
commentWidget False = mempty
|
||||
|
||||
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
|
||||
@ -90,9 +90,9 @@ getDeleteR DeleteRoute{..} = do
|
||||
(deleteFormWdgt, deleteFormEnctype) <- generateFormPost $ confirmForm' drRecords confirmString
|
||||
|
||||
Just targetRoute <- getCurrentRoute
|
||||
|
||||
|
||||
sendResponse =<<
|
||||
defaultLayout $(widgetFile "widgets/delete-confirmation")
|
||||
defaultLayout $(widgetFile "widgets/delete-confirmation/delete-confirmation")
|
||||
|
||||
|
||||
|
||||
|
||||
@ -179,6 +179,9 @@ pointsFieldMax :: (Monad m, HandlerSite m ~ UniWorX) => Maybe Points -> Field m
|
||||
pointsFieldMax Nothing = pointsField
|
||||
pointsFieldMax (Just maxp) = checkBool (<= maxp) (MsgPointsTooHigh maxp) pointsField
|
||||
|
||||
matriculationField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Text
|
||||
matriculationField = textField -- no restrictions, since not everyone has a matriculation and pupils need special tags here
|
||||
|
||||
termsActiveField :: Field Handler TermId
|
||||
termsActiveField = selectField $ optionsPersistKey [TermActive ==. True] [Desc TermStart] termName
|
||||
|
||||
@ -588,9 +591,9 @@ multiAction acts defAction = do
|
||||
widgets <- mapM mToWidget results
|
||||
let actionWidgets = Map.foldrWithKey accWidget [] widgets
|
||||
accWidget _act Nothing = id
|
||||
accWidget act (Just w) = cons $(widgetFile "widgets/multiAction")
|
||||
accWidget act (Just w) = cons $(widgetFile "widgets/multi-action/multi-action")
|
||||
actionResults = Map.map fst results
|
||||
return ((actionResults Map.!) =<< actionRes, $(widgetFile "widgets/multiActionCollect"))
|
||||
return ((actionResults Map.!) =<< actionRes, $(widgetFile "widgets/multi-action/multi-action-collect"))
|
||||
|
||||
multiActionA :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action)
|
||||
=> FieldSettings UniWorX
|
||||
|
||||
@ -25,7 +25,7 @@ gradeSummaryWidget title sts =
|
||||
hasMarkedPasses = positiveSum $ numMarkedPasses sumSummaries
|
||||
hasPoints = positiveSum $ numSheetsPoints sumSummaries
|
||||
hasMarkedPoints = positiveSum $ numMarkedPoints sumSummaries
|
||||
rowWdgts = [ $(widgetFile "widgets/gradingSummaryRow")
|
||||
rowWdgts = [ $(widgetFile "widgets/grading-summary/grading-summary-row")
|
||||
| (sumHeader,summary) <-
|
||||
[ (MsgSheetTypeNormal' ,normalSummary)
|
||||
, (MsgSheetTypeBonus' ,bonusSummary)
|
||||
@ -33,4 +33,4 @@ gradeSummaryWidget title sts =
|
||||
] ]
|
||||
in if 0 == numSheets sumSummaries
|
||||
then mempty
|
||||
else $(widgetFile "widgets/gradingSummary")
|
||||
else $(widgetFile "widgets/grading-summary/grading-summary")
|
||||
|
||||
@ -34,6 +34,18 @@ timeCell t = cell $ formatTime SelFormatDateTime t >>= toWidget
|
||||
userCell :: IsDBTable m a => Text -> Text -> DBCell m a
|
||||
userCell displayName surname = cell $ nameWidget displayName surname
|
||||
|
||||
cellHasUser :: (IsDBTable m a, HasUser c) => c -> DBCell m a
|
||||
cellHasUser = liftA2 userCell (view _userDisplayName) (view _userSurname)
|
||||
|
||||
cellHasUserLink :: (IsDBTable m a, HasEntity u User) => (CryptoUUIDUser -> Route UniWorX) -> u -> DBCell m a
|
||||
cellHasUserLink toLink user =
|
||||
let uid = user ^. _entityKey
|
||||
nWdgt = nameWidget (user ^. _entityVal . _userDisplayName) (user ^. _entityVal . _userSurname)
|
||||
in anchorCellM (toLink <$> encrypt uid) nWdgt
|
||||
|
||||
cellHasMatrikelnummer :: (IsDBTable m a, HasUser c) => c -> DBCell m a
|
||||
cellHasMatrikelnummer = maybe mempty textCell . view _userMatrikelnummer
|
||||
|
||||
-- Just for documentation purposes; inline this code instead:
|
||||
maybeTimeCell :: IsDBTable m a => Maybe UTCTime -> DBCell m a
|
||||
maybeTimeCell = maybe mempty timeCell
|
||||
@ -110,3 +122,10 @@ correctorStateCell sc =
|
||||
correctorLoadCell :: IsDBTable m a => SheetCorrector -> DBCell m a
|
||||
correctorLoadCell sc =
|
||||
i18nCell $ sheetCorrectorLoad sc
|
||||
|
||||
|
||||
commentCell :: IsDBTable m a => Maybe (Route UniWorX) -> DBCell m a
|
||||
commentCell Nothing = mempty
|
||||
commentCell (Just link) = anchorCell link icon
|
||||
where
|
||||
icon = commentWidget True
|
||||
|
||||
@ -576,7 +576,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
||||
, let t' = toPathPiece $ SortingSetting t d
|
||||
]
|
||||
wIdent :: Text -> Text
|
||||
wIdent = toPathPiece . WithIdent dbtIdent
|
||||
wIdent = toPathPiece . WithIdent dbtIdent
|
||||
dbsAttrs'
|
||||
| not $ null dbtIdent = ("id", dbtIdent) : dbsAttrs
|
||||
| otherwise = dbsAttrs
|
||||
@ -802,7 +802,7 @@ cellTooltip :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a -
|
||||
cellTooltip msg = cellContents.mapped %~ (<> tipWdgt)
|
||||
where
|
||||
tipWdgt = [whamlet|
|
||||
<div .js-tooltip>
|
||||
<div .tooltip>
|
||||
<div .tooltip__handle>
|
||||
<div .tooltip__content>_{msg}
|
||||
|]
|
||||
|
||||
@ -9,7 +9,7 @@ modal modalTrigger modalContent = do
|
||||
let modalDynamic = isLeft modalContent
|
||||
modalId <- newIdent
|
||||
triggerId <- newIdent
|
||||
$(widgetFile "widgets/modal")
|
||||
$(widgetFile "widgets/modal/modal")
|
||||
case modalContent of
|
||||
Left route -> do
|
||||
route' <- toTextUrl route
|
||||
|
||||
@ -2,7 +2,9 @@ module Jobs.Handler.QueueNotification
|
||||
( dispatchJobQueueNotification
|
||||
) where
|
||||
|
||||
import Import
|
||||
import Import hiding ((\\))
|
||||
|
||||
import Data.List ((\\))
|
||||
|
||||
import Jobs.Types
|
||||
|
||||
@ -20,7 +22,7 @@ dispatchJobQueueNotification jNotification = runDBJobs . setSerializable $ do
|
||||
guard $ notificationAllowed userNotificationSettings nClass
|
||||
return uid
|
||||
|
||||
|
||||
|
||||
determineNotificationCandidates :: Notification -> DB [Entity User]
|
||||
determineNotificationCandidates NotificationSubmissionRated{..}
|
||||
= E.select . E.from $ \(user `E.InnerJoin` submissionUser) -> do
|
||||
@ -40,20 +42,34 @@ determineNotificationCandidates NotificationSheetSoonInactive{..}
|
||||
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
|
||||
return user
|
||||
determineNotificationCandidates NotificationSheetInactive{..}
|
||||
= E.select . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` sheet) -> do
|
||||
E.on $ lecturer E.^. LecturerCourse E.==. sheet E.^. SheetCourse
|
||||
= E.select . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` sheet) -> do
|
||||
E.on $ lecturer E.^. LecturerCourse E.==. sheet E.^. SheetCourse
|
||||
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
|
||||
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
|
||||
return user
|
||||
determineNotificationCandidates NotificationCorrectionsAssigned{..}
|
||||
= selectList [UserId ==. nUser] []
|
||||
= selectList [UserId ==. nUser] []
|
||||
determineNotificationCandidates NotificationCorrectionsNotDistributed{nSheet}
|
||||
= E.select . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` sheet) -> do
|
||||
E.on $ lecturer E.^. LecturerCourse E.==. sheet E.^. SheetCourse
|
||||
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
|
||||
E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
|
||||
return user
|
||||
|
||||
determineNotificationCandidates NotificationUserRightsUpdate{..}
|
||||
= do
|
||||
-- always send to affected user
|
||||
affectedUser <- selectList [UserId ==. nUser] []
|
||||
-- send to same-school admins only if there was an update
|
||||
currentAdminSchools <- fmap (userAdminSchool . entityVal) <$> selectList [UserAdminUser ==. nUser] []
|
||||
let oldAdminSchools = [ SchoolKey ssh | (ssh, True, _) <- originalRights ]
|
||||
newAdminSchools = currentAdminSchools \\ oldAdminSchools
|
||||
affectedAdmins <- E.select . E.from $ \(user `E.InnerJoin` admin) -> do
|
||||
E.on $ admin E.^. UserAdminUser E.==. user E.^. UserId
|
||||
E.where_ $ admin E.^. UserAdminSchool `E.in_` E.valList newAdminSchools
|
||||
return user
|
||||
return $ affectedUser <> affectedAdmins
|
||||
|
||||
|
||||
classifyNotification :: Notification -> DB NotificationTrigger
|
||||
classifyNotification NotificationSubmissionRated{..} = do
|
||||
Sheet{sheetType} <- belongsToJust submissionSheet =<< getJust nSubmission
|
||||
@ -65,5 +81,6 @@ classifyNotification NotificationSheetSoonInactive{} = return NTSheetSoonInactiv
|
||||
classifyNotification NotificationSheetInactive{} = return NTSheetInactive
|
||||
classifyNotification NotificationCorrectionsAssigned{} = return NTCorrectionsAssigned
|
||||
classifyNotification NotificationCorrectionsNotDistributed{} = return NTCorrectionsNotDistributed
|
||||
classifyNotification NotificationUserRightsUpdate{} = return NTUserRightsUpdate
|
||||
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import Jobs.Handler.SendNotification.SheetActive
|
||||
import Jobs.Handler.SendNotification.SheetInactive
|
||||
import Jobs.Handler.SendNotification.CorrectionsAssigned
|
||||
import Jobs.Handler.SendNotification.CorrectionsNotDistributed
|
||||
import Jobs.Handler.SendNotification.UserRightsUpdate
|
||||
|
||||
|
||||
dispatchJobSendNotification :: UserId -> Notification -> Handler ()
|
||||
|
||||
@ -12,6 +12,8 @@ import Handler.Utils.Mail
|
||||
import Text.Hamlet
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
|
||||
import qualified Database.Esqueleto as E
|
||||
|
||||
dispatchNotificationSheetSoonInactive :: SheetId -> UserId -> Handler ()
|
||||
dispatchNotificationSheetSoonInactive nSheet jRecipient = userMailT jRecipient $ do
|
||||
(Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do
|
||||
@ -33,10 +35,16 @@ dispatchNotificationSheetSoonInactive nSheet jRecipient = userMailT jRecipient $
|
||||
|
||||
dispatchNotificationSheetInactive :: SheetId -> UserId -> Handler ()
|
||||
dispatchNotificationSheetInactive nSheet jRecipient = userMailT jRecipient $ do
|
||||
(Course{..}, Sheet{..}) <- liftHandlerT . runDB $ do
|
||||
(Course{..}, Sheet{..}, nrSubs, nrSubmitters) <- liftHandlerT . runDB $ do
|
||||
sheet <- getJust nSheet
|
||||
course <- belongsToJust sheetCourse sheet
|
||||
return (course, sheet)
|
||||
nrSubs <- count [SubmissionSheet ==. nSheet]
|
||||
(E.Value nrSubmitters:_) <- E.select . E.from $ \(subUser `E.InnerJoin` submission) -> do
|
||||
E.on $ subUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
|
||||
E.where_ $ submission E.^. SubmissionSheet E.==. E.val nSheet
|
||||
-- E.distinctOn [E.don (subUser E.^. SubmissionUserUser)] -- Not necessary due to UniqueSubmisionUser
|
||||
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
||||
return (course, sheet, nrSubs, nrSubmitters)
|
||||
setSubjectI $ MsgMailSubjectSheetInactive courseShorthand sheetName
|
||||
|
||||
MsgRenderer mr <- getMailMsgRenderer
|
||||
@ -49,4 +57,4 @@ dispatchNotificationSheetInactive nSheet jRecipient = userMailT jRecipient $ do
|
||||
addAlternatives $ do
|
||||
let editNotifications = $(ihamletFile "templates/mail/editNotifications.hamlet")
|
||||
providePreferredAlternative ($(ihamletFile "templates/mail/sheetInactive.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
||||
|
||||
|
||||
|
||||
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))
|
||||
|
||||
@ -18,7 +18,7 @@ data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notifica
|
||||
| JobHelpRequest { jSender :: Either (Maybe Address) UserId
|
||||
, jRequestTime :: UTCTime
|
||||
, jHelpRequest :: Text, jReferer :: Maybe Text }
|
||||
| JobSetLogSettings { jInstance :: InstanceId, jLogSettings :: LogSettings }
|
||||
| JobSetLogSettings { jInstance :: InstanceId, jLogSettings :: LogSettings }
|
||||
| JobDistributeCorrections { jSheet :: SheetId }
|
||||
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
||||
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
||||
@ -27,6 +27,7 @@ data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
||||
| NotificationSheetInactive { nSheet :: SheetId }
|
||||
| NotificationCorrectionsAssigned { nUser :: UserId, nSheet :: SheetId }
|
||||
| NotificationCorrectionsNotDistributed { nSheet :: SheetId }
|
||||
| NotificationUserRightsUpdate { nUser :: UserId, originalRights :: [(SchoolShorthand,Bool,Bool)] } -- User rights (admin, lecturer,...) were changed somehow
|
||||
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
||||
|
||||
instance Hashable Job
|
||||
|
||||
@ -559,6 +559,7 @@ data NotificationTrigger = NTSubmissionRatedGraded
|
||||
| NTSheetInactive
|
||||
| NTCorrectionsAssigned
|
||||
| NTCorrectionsNotDistributed
|
||||
| NTUserRightsUpdate
|
||||
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||
|
||||
instance Universe NotificationTrigger
|
||||
@ -590,6 +591,7 @@ instance Default NotificationSettings where
|
||||
NTSheetInactive -> True
|
||||
NTCorrectionsAssigned -> True
|
||||
NTCorrectionsNotDistributed -> True
|
||||
NTUserRightsUpdate -> True
|
||||
|
||||
instance ToJSON NotificationSettings where
|
||||
toJSON v = toJSON . HashMap.fromList $ map (id &&& notificationAllowed v) universeF
|
||||
@ -607,7 +609,6 @@ derivePersistFieldJSON ''NotificationSettings
|
||||
instance ToBackendKey SqlBackend record => Hashable (Key record) where
|
||||
hashWithSalt s key = s `hashWithSalt` fromSqlKey key
|
||||
|
||||
|
||||
derivePersistFieldJSON ''MailLanguages
|
||||
|
||||
|
||||
|
||||
@ -10,15 +10,12 @@ import qualified Data.Set as Set
|
||||
import qualified Database.Esqueleto as E
|
||||
-- import Database.Persist -- currently not needed here
|
||||
|
||||
-- ezero = E.val (0 :: Int64)
|
||||
|
||||
emptyOrIn :: PersistField typ =>
|
||||
E.SqlExpr (E.Value typ) -> Set typ -> E.SqlExpr (E.Value Bool)
|
||||
emptyOrIn criterion testSet
|
||||
| Set.null testSet = E.val True
|
||||
| otherwise = criterion `E.in_` E.valList (Set.toList testSet)
|
||||
|
||||
|
||||
entities2map :: PersistEntity record => [Entity record] -> Map (Key record) record
|
||||
entities2map = foldl' (\m entity -> Map.insert (entityKey entity) (entityVal entity) m) Map.empty
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize
|
||||
renderAForm :: Monad m => FormLayout -> FormRender m a
|
||||
renderAForm formLayout aform fragment = do
|
||||
(res, ($ []) -> fieldViews) <- aFormToForm aform
|
||||
let widget = $(widgetFile "widgets/form")
|
||||
let widget = $(widgetFile "widgets/form/form")
|
||||
return (res, widget)
|
||||
|
||||
-- | special id to identify form section headers, see 'aformSection' and 'formSection'
|
||||
@ -407,7 +407,7 @@ reorderField optList = Field{..}
|
||||
isSel n = (==) (either (const $ map optionInternalValue olOptions) id val !! pred n) . optionInternalValue
|
||||
nums = map (id &&& withNum theId) [1..length olOptions]
|
||||
withNum t n = tshow n <> "." <> t
|
||||
$(widgetFile "widgets/permutation")
|
||||
$(widgetFile "widgets/permutation/permutation")
|
||||
|
||||
optionsFinite :: ( MonadHandler m
|
||||
, Finite a
|
||||
|
||||
@ -3,7 +3,7 @@ module Utils.Lens ( module Utils.Lens ) where
|
||||
import Import.NoFoundation
|
||||
import Control.Lens as Utils.Lens hiding ((<.>))
|
||||
import Control.Lens.Extras as Utils.Lens (is)
|
||||
import Utils.Lens.TH as Utils.Lens (makeLenses_)
|
||||
import Utils.Lens.TH as Utils.Lens (makeLenses_, makeClassyFor_)
|
||||
|
||||
import qualified Database.Esqueleto as E (Value(..),InnerJoin(..))
|
||||
|
||||
@ -26,9 +26,35 @@ _InnerJoinRight :: Lens' (E.InnerJoin l r) r
|
||||
_InnerJoinRight f (E.InnerJoin l r) = (l `E.InnerJoin`) <$> f r
|
||||
|
||||
|
||||
makeLenses_ ''Entity
|
||||
-- makeLenses_ ''Entity
|
||||
makeClassyFor_ "HasEntity" "hasEntity" ''Entity
|
||||
-- class HasEntity c record | c -> record where
|
||||
-- hasEntity :: Lens' c (Entity record)
|
||||
|
||||
-- makeLenses_ ''Course
|
||||
makeClassyFor_ "HasCourse" "hasCourse" ''Course
|
||||
-- class HasCourse c where
|
||||
-- hasCourse :: Lens' c Course
|
||||
|
||||
instance (HasCourse a) => HasCourse (Entity a) where
|
||||
hasCourse = _entityVal . hasCourse
|
||||
|
||||
makeClassyFor_ "HasUser" "hasUser" ''User
|
||||
-- > :info HasUser
|
||||
-- class HasUser c where {-# MINIMAL hasUser #-}
|
||||
-- hasUser :: Lens' c User
|
||||
-- _userDisplayName :: Lens' c Text
|
||||
-- _userSurname :: Lens' c Text
|
||||
-- _user...
|
||||
--
|
||||
|
||||
-- TODO: Is this instance needed?
|
||||
instance (HasUser a) => HasUser (Entity a) where
|
||||
hasUser = _entityVal . hasUser
|
||||
-- This is what we would want instead:
|
||||
-- instance (HasEntity a User) => HasUser a where
|
||||
-- hasUser = _entityVal
|
||||
|
||||
makeLenses_ ''Course
|
||||
|
||||
makeLenses_ ''SheetCorrector
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
module Utils.Lens.TH where
|
||||
|
||||
import ClassyPrelude (String, Maybe(..))
|
||||
import Control.Lens
|
||||
import Control.Lens.Internal.FieldTH
|
||||
import Language.Haskell.TH
|
||||
@ -15,6 +16,13 @@ lensRules_ :: LensRules
|
||||
lensRules_ = lensRules
|
||||
& lensField .~ \_ _ n -> [TopName (mkName ('_':nameBase n))]
|
||||
|
||||
-- | Like lensRules_, but different class and function name
|
||||
classyRulesFor_ :: ClassyNamer -> LensRules
|
||||
classyRulesFor_ clsNamer = classyRules
|
||||
& lensClass .~ clsNamer
|
||||
& lensField .~ \_ _ n -> [TopName (mkName ('_':nameBase n))]
|
||||
|
||||
|
||||
-- | Build lenses (and traversals) with a sensible default configuration.
|
||||
-- Works the same as 'makeLenses' except that
|
||||
-- the resulting lens is also prefixed with an underscore.
|
||||
@ -42,6 +50,14 @@ lensRules_ = lensRules
|
||||
-- @
|
||||
-- 'makeLenses_' = 'makeLensesWith' 'lensRules_'
|
||||
-- @
|
||||
|
||||
makeLenses_ :: Name -> DecsQ
|
||||
makeLenses_ = makeFieldOptics lensRules_
|
||||
|
||||
-- | like makeClassyFor but only specifies names for class and its function,
|
||||
-- otherwise lenses are created with underscore like `makeLenses_`
|
||||
makeClassyFor_ :: String -> String -> Name -> DecsQ
|
||||
makeClassyFor_ clsName funName = makeFieldOptics (classyRulesFor_ clNamer)
|
||||
where
|
||||
clNamer :: ClassyNamer
|
||||
-- clNamer _ = Just (clsName, funName) -- for newer versions >= 4.17
|
||||
clNamer _ = Just (mkName clsName, mkName funName)
|
||||
77
src/index.md
77
src/index.md
@ -1,40 +1,45 @@
|
||||
# Datei Index
|
||||
|
||||
Database,Esqueleto.*
|
||||
: Hilfsdefinitionen, welche Esqueleto anbieten könnte
|
||||
|
||||
Utils, Utils.*
|
||||
: Hilfsfunktionionen _unabhängig von Foundation_
|
||||
|
||||
|
||||
Utils
|
||||
: Yesod Hilfsfunktionen und Instanzen, Text-HTML-Widget-Konvertierungen
|
||||
(`DisplayAble`), Crud, `NTop`, Utility-Funktionen für `MonadPlus`, `Maybe`,
|
||||
`MaybeT`, `Map`, und Attrs-Lists
|
||||
|
||||
|
||||
Utils.TH
|
||||
: Template Haskell code-generatoren von unabhängigen Hilfsfunktionen (`deriveSimpleWith`)
|
||||
|
||||
|
||||
Utils.DB
|
||||
: Derived persistent functions (`existsBy`, `getKeyBy404`, ...)
|
||||
|
||||
|
||||
Utils.Form
|
||||
: `renderAForm`, Field-Settings helper, `FormIdentifier`, `Button`-Klasse,
|
||||
unabhängige konkrete Buttons
|
||||
|
||||
|
||||
Utils.PathPiece
|
||||
: (Template-Haskell)-Hilfsfunktionen für Formulierung von PathPiece-Instanzen
|
||||
|
||||
|
||||
Utils.Message
|
||||
: redefines addMessage, addMessageI, defines MessageClass
|
||||
|
||||
Utils.Lens
|
||||
: Automatisch erzeugt Linsen für eigene und Yesod-Typen, `Control.Lens`-Export
|
||||
|
||||
|
||||
Utils.DateTime
|
||||
: Template Haskell code-generatoren zum compile-time einbinden von Zeitzone
|
||||
und `TimeLocale`
|
||||
|
||||
|
||||
Handler.Utils, Handler.Utils.*
|
||||
: Hilfsfunktionien, importieren `Import`
|
||||
|
||||
|
||||
Handler.Utils
|
||||
: `Handler.Utils.*`, Unsortierte _Foundation-abhängige_ Hilfsfunktionen
|
||||
|
||||
|
||||
Handler.Utils.DateTime
|
||||
: Nutzer-spezifisches `DateTime`-Formatieren
|
||||
|
||||
@ -42,39 +47,39 @@ Handler.Utils.Form
|
||||
: Konkrete Buttons, spezielle Felder (inkl. Datei-Upload-Felder),
|
||||
Optionslisten (`optionsPersistCryptoId`), `forced`-Felder (erzwungenes
|
||||
Ergebnis, deaktiviertes Feld), `multiAction`
|
||||
|
||||
|
||||
Handler.Utils.Rating
|
||||
: `Rating` (kodiert eine Rating-Datei), Parsen und PrettyPrinten von
|
||||
Rating-Dateien
|
||||
|
||||
|
||||
Handler.Utils.Sheet
|
||||
: `fetchSheet`
|
||||
|
||||
|
||||
Handler.Utils.StudyFeatures
|
||||
: Parsen von LDAP StudyFeatures-Strings
|
||||
|
||||
|
||||
Handler.Utils.Submission
|
||||
: `assignSubmissions`, `sinkSubmission` State-Maschinen die (bereits geparste)
|
||||
ZIP-Archive auseinandernehmen und (in einer Transaction) in die Datenbank
|
||||
speichern
|
||||
|
||||
|
||||
Handler.Utils.Submission.TH
|
||||
: Template Haskell zum parsen und einkompilieren von Dateiname-Blacklist für
|
||||
`sinkSubmission`; Patterns in `config/submission-blacklist`
|
||||
|
||||
|
||||
Handler.Utils.Table
|
||||
: Hilfsfunktion zum direkten Benutzen von Colonnade (kein `dbTable`)
|
||||
|
||||
|
||||
Handler.Utils.Table.Pagination
|
||||
: Here be Dragons
|
||||
|
||||
|
||||
Paginated database-backed tables with support for sorting, filtering,
|
||||
numbering, forms, further database-requests within cells
|
||||
|
||||
|
||||
Includes helper functions for mangling pagination-, sorting-, and filter-settings
|
||||
|
||||
|
||||
Includes helper functions for constructing common types of cells
|
||||
|
||||
|
||||
Handler.Utils.Table.Pagination.Types
|
||||
: `Sortable`-Headedness for colonnade
|
||||
|
||||
@ -83,17 +88,17 @@ Handler.Utils.Table.Cells
|
||||
|
||||
Handler.Utils.Templates
|
||||
: Modals
|
||||
|
||||
|
||||
Handler.Utils.Zip
|
||||
: Conduit-basiertes ZIP Parsen und Erstellen
|
||||
|
||||
|
||||
Handler.Common
|
||||
: Handler aus dem Scaffolding; Implementierungen von Handlern, die _jede
|
||||
Website_ irgendwann braucht
|
||||
|
||||
|
||||
CryptoID
|
||||
: Definiert CryptoIDs für custom Typen (aus Model)
|
||||
|
||||
|
||||
Model.Migration
|
||||
: Manuelle Datenbank-Migration
|
||||
|
||||
@ -103,43 +108,43 @@ Model.Rating
|
||||
Jobs
|
||||
: `handleJobs` worker thread handling background jobs
|
||||
`JobQueueException`
|
||||
|
||||
|
||||
Jobs.Types
|
||||
: `Job`, `Notification`, `JobCtl` Types of Jobs
|
||||
|
||||
|
||||
Cron.Types
|
||||
: Datentypen zur Spezifikation von Intervallen zu denen Jobs ausgeführt werden
|
||||
können:
|
||||
|
||||
|
||||
`Cron`, `CronMatch`, `CronAbsolute`, `CronRepeat`, `Crontab`
|
||||
|
||||
|
||||
Cron
|
||||
: Seiteneffektfreie Berechnungen auf Typen aus `Cron.Types`: `nextCronMatch`
|
||||
|
||||
|
||||
Jobs.Queue
|
||||
: Funktionen zum _anstoßen_ von Jobs und zur Kommunikation mit den
|
||||
Worker-Threads
|
||||
|
||||
|
||||
`writeJobCtl` schickt Nachricht an einen pseudo-Zufälligen worker-thread der
|
||||
lokalen Instanz
|
||||
|
||||
|
||||
`queueJob` und `queueJob'` schreiben neue Jobs in die Instanz-übergreifende
|
||||
Job-Queue, `queueJob'` stößt außerdem einen lokalen worker-thread an sich
|
||||
des Jobs anzunehmen
|
||||
|
||||
|
||||
`runDBJobs` ersetzt `runDB` und erlaubt `queueDBJob` zu
|
||||
benutzen. `queueDBJob` schreibt einen Job in die Queue; am Ende stößt
|
||||
`runDBJobs` lokale worker-threads für alle mit `queueDBJobs` eingetragenen
|
||||
Jobs an.
|
||||
|
||||
|
||||
Jobs.TH
|
||||
: Templatehaskell für den dispatch mechanismus für `Jobs`
|
||||
|
||||
|
||||
Jobs.Crontab
|
||||
: Generiert `Crontab JobCtl` aus der Datenbank (sammelt alle in den Daten aus
|
||||
der Datenbank impliziten Jobs (notifications zu bestimmten zeiten,
|
||||
aufräumaktionen, ...) ein)
|
||||
|
||||
|
||||
Jobs.Handler.**
|
||||
: Via `Jobs.TH` delegiert `Jobs` das Interpretieren und Ausführen eines Werts
|
||||
aus `Jobs.Types` an einen dieser Handler
|
||||
|
||||
@ -4,12 +4,11 @@
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
flex: 0 0 0;
|
||||
flex-basis: var(--asidenav-width-lg, 20%);
|
||||
min-height: calc(100% - var(--header-height));
|
||||
transition: all .2s ease-out;
|
||||
width: var(--asidenav-width-lg, 20%);
|
||||
height: 100%;
|
||||
flex: 0 0 0;
|
||||
flex-basis: var(--asidenav-width-lg, 20%);
|
||||
transition: all .2s ease-out;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
@ -72,6 +71,14 @@
|
||||
|
||||
.asidenav {
|
||||
color: var(--color-font);
|
||||
min-height: calc(100% - var(--header-height));
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.asidenav__box {
|
||||
@ -183,9 +190,7 @@
|
||||
/* LIST-ITEM */
|
||||
|
||||
.asidenav__list-item {
|
||||
position: relative;
|
||||
color: var(--color-font);
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
@ -207,10 +212,8 @@
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.asidenav__nested-list {
|
||||
transform: translateX(100%);
|
||||
opacity: 1;
|
||||
width: 200px;
|
||||
.asidenav__nested-list-wrapper {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -242,7 +245,7 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding: 7px 10px;
|
||||
padding: 8px 3px;
|
||||
justify-content: flex-start;
|
||||
color: var(--color-font);
|
||||
width: 100%;
|
||||
@ -258,17 +261,17 @@
|
||||
}
|
||||
|
||||
/* hover sub-menus */
|
||||
.asidenav__nested-list {
|
||||
.asidenav__nested-list-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
color: var(--color-font);
|
||||
transform: translateX(0);
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
box-shadow: 0 0 13px rgba(0, 0, 0, 0.4);
|
||||
background-color: var(--color-grey-light);
|
||||
box-shadow: 1px 1px 1px 0px var(--color-grey);
|
||||
}
|
||||
|
||||
.asidenav__nested-list {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 425px) {
|
||||
@ -280,19 +283,16 @@
|
||||
|
||||
.asidenav__nested-list-item {
|
||||
position: relative;
|
||||
color: var(--color-lightwhite);
|
||||
background-color: var(--color-dark);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-darker);
|
||||
background-color: var(--color-lightwhite);
|
||||
}
|
||||
|
||||
.asidenav__link-wrapper {
|
||||
padding-left: 13px;
|
||||
padding-right: 13px;
|
||||
border-left: 20px solid white;
|
||||
transition: all .2s ease;
|
||||
color: var(--color-lightwhite);
|
||||
color: var(--color-font);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
.js-show-hide {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.js-show-hide__toggle {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
@ -17,7 +17,7 @@
|
||||
text-align: center;
|
||||
padding: 0 13px;
|
||||
margin: 0 2px;
|
||||
background-color: #b3b7c1;
|
||||
background-color: var(--color-dark);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
@ -35,5 +35,5 @@
|
||||
.tab-opener.tab-visible {
|
||||
background-color: transparent;
|
||||
color: rgb(52, 48, 58);
|
||||
border-bottom-color: #5F98C2;
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
.js-tooltip {
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 1.5rem;
|
||||
@ -67,7 +67,7 @@
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.js-tooltip {
|
||||
.tooltip {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
window.utils = window.utils || {};
|
||||
|
||||
var ALERTS_CLASS = 'alerts';
|
||||
var ALERTS_TOGGLER_CLASS = 'alerts__toggler';
|
||||
var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible';
|
||||
var ALERTS_TOGGLER_APPEAR_DELAY = 120;
|
||||
@ -18,7 +19,11 @@
|
||||
window.utils.alerts = function(alertsEl) {
|
||||
|
||||
if (alertsEl.classList.contains(JS_INITIALIZED_CLASS)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!alertsEl || !alertsEl.classList.contains(ALERTS_CLASS)) {
|
||||
throw new Error('utils.alerts has to be called with alerts element');
|
||||
}
|
||||
|
||||
var togglerCheckRequested = false;
|
||||
|
||||
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 || {};
|
||||
|
||||
var JS_INITIALIZED = 'js-initialized';
|
||||
var SUBMIT_BUTTON_SELECTOR = '[type="submit"]:not([formnovalidate])';
|
||||
var AUTOSUBMIT_BUTTON_SELECTOR = '[type="submit"][data-autosubmit]';
|
||||
|
||||
function formValidator(inputs) {
|
||||
var done = true;
|
||||
inputs.forEach(function(inp) {
|
||||
var len = inp.value.trim().length;
|
||||
if (done && len === 0) {
|
||||
done = false;
|
||||
}
|
||||
});
|
||||
return done;
|
||||
}
|
||||
|
||||
window.utils.form = function(form, options) {
|
||||
|
||||
if (form.classList.contains(JS_INITIALIZED)) {
|
||||
return false;
|
||||
}
|
||||
form.classList.add(JS_INITIALIZED);
|
||||
|
||||
// reactive buttons
|
||||
var submitBtn = form.querySelector(SUBMIT_BUTTON_SELECTOR);
|
||||
if (submitBtn) {
|
||||
window.utils.setup('reactiveButton', form, { button: submitBtn });
|
||||
}
|
||||
|
||||
// conditonal fieldsets
|
||||
var fieldSets = Array.from(form.querySelectorAll('fieldset[data-conditional-id][data-conditional-value]'));
|
||||
window.utils.setup('interactiveFieldset', form, { fieldSets });
|
||||
|
||||
// hide autoSubmit submit button
|
||||
window.utils.setup('autoSubmit', form, options);
|
||||
};
|
||||
|
||||
// registers input-listener for each element in <inputs> (array) and
|
||||
// enables <button> if <validation> for these inputs returns true
|
||||
window.utils.reactiveButton = function(form, button, validation) {
|
||||
// enables <button> if <formValidator> for these inputs returns true
|
||||
window.utils.reactiveButton = function(form, options) {
|
||||
options = options || {};
|
||||
var button = options.button;
|
||||
var requireds = Array.from(form.querySelectorAll('[required]'));
|
||||
|
||||
if (!button) {
|
||||
throw new Error('Please provide both a button to reactiveButton');
|
||||
}
|
||||
|
||||
if (requireds.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof button.dataset.formnorequired !== 'undefined' && button.dataset.formnorequired !== null) {
|
||||
button.addEventListener('click', function() {
|
||||
form.submit();
|
||||
@ -27,7 +71,7 @@
|
||||
});
|
||||
|
||||
function updateButtonState() {
|
||||
if (validation.call(null, requireds) === true) {
|
||||
if (formValidator(requireds) === true) {
|
||||
button.removeAttribute('disabled');
|
||||
} else {
|
||||
button.setAttribute('disabled', 'true');
|
||||
@ -35,7 +79,14 @@
|
||||
}
|
||||
};
|
||||
|
||||
window.utils.interactiveFieldset = function(form, fieldSets) {
|
||||
window.utils.interactiveFieldset = function(form, options) {
|
||||
options = options || {};
|
||||
var fieldSets = options.fieldSets;
|
||||
|
||||
if (!fieldSets) {
|
||||
throw new Error('interactiveFieldset must be passed fieldSets via options');
|
||||
}
|
||||
|
||||
var fields = fieldSets.map(function(fs) {
|
||||
return {
|
||||
fieldSet: fs,
|
||||
@ -64,58 +115,11 @@
|
||||
updateFields();
|
||||
}
|
||||
};
|
||||
|
||||
window.utils.autoSubmit = function(form, options) {
|
||||
var button = form.querySelector(AUTOSUBMIT_BUTTON_SELECTOR);
|
||||
if (button) {
|
||||
button.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
document.addEventListener('setup', function(e) {
|
||||
if (e.detail.module && e.detail.module !== 'showHide')
|
||||
return;
|
||||
|
||||
var forms = e.detail.scope.querySelectorAll('form');
|
||||
Array.from(forms).forEach(function(form) {
|
||||
// auto reactiveButton submit-buttons with required fields
|
||||
var submitBtns = Array.from(form.querySelectorAll('[type=submit]:not([formnovalidate])'));
|
||||
submitBtns.forEach(function(submitBtn) {
|
||||
window.utils.reactiveButton(form, submitBtn, validateForm);
|
||||
});
|
||||
|
||||
// auto conditonal fieldsets
|
||||
var fieldSets = Array.from(form.querySelectorAll('fieldset[data-conditional-id][data-conditional-value]'));
|
||||
window.utils.interactiveFieldset(form, fieldSets);
|
||||
});
|
||||
|
||||
function validateForm(inputs) {
|
||||
var done = true;
|
||||
inputs.forEach(function(inp) {
|
||||
var len = inp.value.trim().length;
|
||||
if (done && len === 0) {
|
||||
done = false;
|
||||
}
|
||||
});
|
||||
return done;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'showHide' }, bubbles: true, cancelable: true }))
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('setup', function(e) {
|
||||
if (e.detail.module && e.detail.module !== 'autoSubmit')
|
||||
return;
|
||||
|
||||
Array.from(e.detail.scope.querySelectorAll('[data-autosubmit]:not(.js-initialized)')).forEach(function(elem) {
|
||||
if ((elem instanceof HTMLInputElement && elem.type == 'submit') || (elem instanceof HTMLButtonElement && elem.type == 'submit')) {
|
||||
var ancestor = elem.closest('.form-group');
|
||||
var target = ancestor || elem;
|
||||
|
||||
target.classList.add('hidden');
|
||||
}
|
||||
|
||||
elem.classList.add('js-initialized');
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'autoSubmit' }, bubbles: true, cancelable: true }))
|
||||
});
|
||||
171
static/js/utils/inputs.js
Normal file
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>
|
||||
neue geplante Features:
|
||||
<ul>
|
||||
<li> Stundenplan/Kalender
|
||||
<li> Stundenplan/Kalender mit Veranstaltungen und Klausuren
|
||||
<li> Vollständige Vorlesungshomepages
|
||||
<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>
|
||||
<h1>
|
||||
_{MsgMailSheetInactiveIntro (CI.original courseName) termDesc sheetName}
|
||||
_{MsgMailSheetInactiveIntro (CI.original courseName) termDesc sheetName nrSubs nrSubmitters}
|
||||
<p>
|
||||
<a href=@{CSheetR tid ssh csh shn SShowR}>
|
||||
#{sheetName}
|
||||
|
||||
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>
|
||||
<label .file-container__label.btn for=#{fuiHtmlId}>#{fuiTitle}
|
||||
<div .checkbox>
|
||||
<input .file-container__checkbox.js-file-checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId} type="checkbox">
|
||||
<input .file-container__checkbox.file-checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId} type="checkbox">
|
||||
<label for=#{fuiHtmlId}>
|
||||
|
||||
$# new files
|
||||
@ -15,6 +15,6 @@ $# new files
|
||||
<div .file-input__unpack>
|
||||
<label for=#{fieldId}_zip>ZIPs automatisch entpacken
|
||||
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips}>
|
||||
<div class="js-tooltip">
|
||||
<div class="tooltip">
|
||||
<div class="tooltip__handle">
|
||||
<div class="tooltip__content">Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu.
|
||||
|
||||
@ -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}
|
||||
$case sheetSubmissionMode sheet
|
||||
$of CorrectorSubmissions
|
||||
<div .js-tooltip>
|
||||
<div .tooltip>
|
||||
<div .tooltip__handle>
|
||||
<div .tooltip__content>_{MsgSheetCorrectorSubmissionsTip}
|
||||
$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() {
|
||||
var dbtIdent = #{String $ dbtIdent};
|
||||
var headerDBTableShortcircuit = #{String (toPathPiece HeaderDBTableShortcircuit)};
|
||||
var selector = '#' + dbtIdent + '-table-wrapper:not(.js-initialized)';
|
||||
var wrapper = document.querySelector(selector);
|
||||
|
||||
if (wrapper) {
|
||||
wrapper.dataset.dbtIdent = dbtIdent;
|
||||
window.utils.asyncTable(wrapper);
|
||||
window.utils.setup('asyncTable', wrapper, { headerDBTableShortcircuit, dbtIdent });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
<div .container>
|
||||
<h3>
|
||||
Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
|
||||
|
||||
<section>
|
||||
^{features}
|
||||
@ -8,13 +6,13 @@
|
||||
<section>
|
||||
<h2>
|
||||
Bekannte Bugs
|
||||
<h3>
|
||||
Stand: Februar 2019
|
||||
<ul>
|
||||
<li>
|
||||
Login ist u.U. anders als im alten System, z.B. <span style="font-family:monospace">@campus.lmu.de</span> statt <span style="font-family:monospace">@lmu.de</span>
|
||||
Login ist u.U. anders als im alten System, z.B. momentan geht nur <span style="font-family:monospace">@campus.lmu.de</span> aber nicht die Abkürzung <span style="font-family:monospace">@lmu.de</span>
|
||||
<li>
|
||||
Favicon ist default des Frameworks
|
||||
<li>
|
||||
Format von Bewertungsdateien ist provisorisch
|
||||
Format von Bewertungsdateien ist noch provisorisch
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
@ -22,26 +20,6 @@
|
||||
<p #changelog>
|
||||
#{changeLog}
|
||||
|
||||
<section>
|
||||
<h2>
|
||||
Impressum
|
||||
|
||||
<ul style="list-style-type: none">
|
||||
<li>
|
||||
Dr Steffen Jost
|
||||
<li>
|
||||
<a href="mailto:jost@tcs.ifi.lmu.de">
|
||||
jost@tcs.ifi.lmu.de
|
||||
<li>
|
||||
Lehr- und Forschungseinheit für
|
||||
Theoretische Informatik
|
||||
<li>
|
||||
Institut für Informatik
|
||||
<li>
|
||||
Ludwig-Maximilians-Universität München
|
||||
<li>
|
||||
Oettingenstr. 67, 80538 München
|
||||
|
||||
<section>
|
||||
<p #gitrev>
|
||||
#{gitInfo}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<div #alerts .alerts>
|
||||
<div #alerts-1 .alerts> <!-- make wIdent work here instead of '#alerts-1' -->
|
||||
<div .alerts__toggler>
|
||||
$forall (status, msg) <- mmsgs
|
||||
$with status2 <- bool status "info" (status == "")
|
||||
<div class="alert alert-#{status2}">
|
||||
<div .alert.alert-#{status2}>
|
||||
<div .alert__closer>
|
||||
<div .alert__icon>
|
||||
<div .alert__content>
|
||||
|
||||
@ -1,18 +1,4 @@
|
||||
document.addEventListener('setup', function(e) {
|
||||
if (!e.detail.module || e.detail.module !== 'alerts') {
|
||||
return;
|
||||
}
|
||||
|
||||
// setup alerts
|
||||
if (e.detail.scope.classList.contains('alerts')) {
|
||||
window.utils.alerts(e.detail.scope);
|
||||
} else {
|
||||
var alertsEl = e.detail.scope.querySelector('.alerts');
|
||||
if (alertsEl)
|
||||
window.utils.alerts(alertsEl);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'alerts' }, bubbles: true, cancelable: true }))
|
||||
var alertsElement = document.querySelector('#' + 'alerts-1');
|
||||
window.utils.setup('alerts', alertsElement);
|
||||
});
|
||||
|
||||
@ -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}>
|
||||
<div .asidenav__link-shorthand>#{courseShorthand}
|
||||
<div .asidenav__link-label>#{courseName}
|
||||
<ul .asidenav__nested-list.list--iconless>
|
||||
$forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions
|
||||
$case menuItemType
|
||||
$of PageActionPrime
|
||||
<li .asidenav__nested-list-item>
|
||||
<a .asidenav__link-wrapper href=#{route}>_{menuItemLabel}
|
||||
$of _
|
||||
<div .asidenav__nested-list-wrapper>
|
||||
<ul .asidenav__nested-list.list--iconless>
|
||||
$forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions
|
||||
$case menuItemType
|
||||
$of PageActionPrime
|
||||
<li .asidenav__nested-list-item>
|
||||
<a .asidenav__link-wrapper href=#{route}>_{menuItemLabel}
|
||||
$of _
|
||||
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);
|
||||
});
|
||||
});
|
||||
@ -51,4 +51,4 @@ $#
|
||||
\ (_{title $ getSum $ summary ^. _numSheetsPoints})
|
||||
$# Kurze Alternative mit Hashtag-Symbol für "Anzahl"
|
||||
$# \ (##{display $ summary ^. _numSheetsPoints})
|
||||
<td .table__td>#{display $ summary ^. _numSheets}
|
||||
<td .table__td>#{display $ summary ^. _numSheets}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user