_{msg}
|]
diff --git a/src/Handler/Utils/Templates.hs b/src/Handler/Utils/Templates.hs
index c316d9b77..f29d79fba 100644
--- a/src/Handler/Utils/Templates.hs
+++ b/src/Handler/Utils/Templates.hs
@@ -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
diff --git a/src/Jobs/Handler/QueueNotification.hs b/src/Jobs/Handler/QueueNotification.hs
index 8c9a759a9..5dfdc20fa 100644
--- a/src/Jobs/Handler/QueueNotification.hs
+++ b/src/Jobs/Handler/QueueNotification.hs
@@ -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
diff --git a/src/Jobs/Handler/SendNotification.hs b/src/Jobs/Handler/SendNotification.hs
index 547830603..64921e118 100644
--- a/src/Jobs/Handler/SendNotification.hs
+++ b/src/Jobs/Handler/SendNotification.hs
@@ -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 ()
diff --git a/src/Jobs/Handler/SendNotification/SheetInactive.hs b/src/Jobs/Handler/SendNotification/SheetInactive.hs
index f318539f4..7112e5c39 100644
--- a/src/Jobs/Handler/SendNotification/SheetInactive.hs
+++ b/src/Jobs/Handler/SendNotification/SheetInactive.hs
@@ -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))
-
+
diff --git a/src/Jobs/Handler/SendNotification/UserRightsUpdate.hs b/src/Jobs/Handler/SendNotification/UserRightsUpdate.hs
new file mode 100644
index 000000000..aaf50ac72
--- /dev/null
+++ b/src/Jobs/Handler/SendNotification/UserRightsUpdate.hs
@@ -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))
+
diff --git a/src/Jobs/Types.hs b/src/Jobs/Types.hs
index 2e6eb3164..151d0e404 100644
--- a/src/Jobs/Types.hs
+++ b/src/Jobs/Types.hs
@@ -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
diff --git a/src/Model/Types.hs b/src/Model/Types.hs
index dfedb2663..94655817d 100644
--- a/src/Model/Types.hs
+++ b/src/Model/Types.hs
@@ -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
diff --git a/src/Utils/DB.hs b/src/Utils/DB.hs
index 2b8f8bd01..cb8b80d4e 100644
--- a/src/Utils/DB.hs
+++ b/src/Utils/DB.hs
@@ -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
diff --git a/src/Utils/Form.hs b/src/Utils/Form.hs
index 0391faea1..8c53501f8 100644
--- a/src/Utils/Form.hs
+++ b/src/Utils/Form.hs
@@ -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
diff --git a/src/Utils/Lens.hs b/src/Utils/Lens.hs
index 68a1ba2a8..3fea6ff14 100644
--- a/src/Utils/Lens.hs
+++ b/src/Utils/Lens.hs
@@ -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
diff --git a/src/Utils/Lens/TH.hs b/src/Utils/Lens/TH.hs
index 6f5bf4c14..dffbf10c0 100644
--- a/src/Utils/Lens/TH.hs
+++ b/src/Utils/Lens/TH.hs
@@ -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)
\ No newline at end of file
diff --git a/src/index.md b/src/index.md
index 563023e8b..17798d0df 100644
--- a/src/index.md
+++ b/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
diff --git a/templates/widgets/asidenav.lucius b/static/css/utils/asidenav.scss
similarity index 92%
rename from templates/widgets/asidenav.lucius
rename to static/css/utils/asidenav.scss
index 9123c407d..51fe73163 100644
--- a/templates/widgets/asidenav.lucius
+++ b/static/css/utils/asidenav.scss
@@ -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);
}
}
diff --git a/templates/widgets/form.lucius b/static/css/utils/form.scss
similarity index 100%
rename from templates/widgets/form.lucius
rename to static/css/utils/form.scss
diff --git a/templates/standalone/inputs.lucius b/static/css/utils/inputs.scss
similarity index 100%
rename from templates/standalone/inputs.lucius
rename to static/css/utils/inputs.scss
diff --git a/templates/standalone/showHide.lucius b/static/css/utils/showHide.scss
similarity index 94%
rename from templates/standalone/showHide.lucius
rename to static/css/utils/showHide.scss
index d1413c0fb..64dfe367c 100644
--- a/templates/standalone/showHide.lucius
+++ b/static/css/utils/showHide.scss
@@ -1,7 +1,3 @@
-.js-show-hide {
- position: relative;
-}
-
.js-show-hide__toggle {
position: relative;
cursor: pointer;
diff --git a/static/css/utils/tabber.css b/static/css/utils/tabber.scss
similarity index 89%
rename from static/css/utils/tabber.css
rename to static/css/utils/tabber.scss
index 6f823b410..d135a5f27 100644
--- a/static/css/utils/tabber.css
+++ b/static/css/utils/tabber.scss
@@ -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);
}
diff --git a/templates/standalone/tooltip.lucius b/static/css/utils/tooltip.scss
similarity index 97%
rename from templates/standalone/tooltip.lucius
rename to static/css/utils/tooltip.scss
index 0a2154768..7e538e46a 100644
--- a/templates/standalone/tooltip.lucius
+++ b/static/css/utils/tooltip.scss
@@ -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;
diff --git a/static/js/utils/alerts.js b/static/js/utils/alerts.js
index eec2fb73a..d52a47376 100644
--- a/static/js/utils/alerts.js
+++ b/static/js/utils/alerts.js
@@ -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;
diff --git a/static/js/utils/asidenav.js b/static/js/utils/asidenav.js
new file mode 100644
index 000000000..154232109
--- /dev/null
+++ b/static/js/utils/asidenav.js
@@ -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();
+ };
+})();
diff --git a/static/js/utils/asyncTable.js b/static/js/utils/asyncTable.js
new file mode 100644
index 000000000..fe164d3b2
--- /dev/null
+++ b/static/js/utils/asyncTable.js
@@ -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();
+ };
+})();
diff --git a/static/js/utils/featureChecker.js b/static/js/utils/featureChecker.js
deleted file mode 100644
index ad8e26303..000000000
--- a/static/js/utils/featureChecker.js
+++ /dev/null
@@ -1,4 +0,0 @@
-window.addEventListener('touchstart', function onFirstTouch() {
- document.body.classList.add('touch-supported');
- window.removeEventListener('touchstart', onFirstTouch, false);
-}, false);
diff --git a/templates/widgets/form.julius b/static/js/utils/form.js
similarity index 51%
rename from templates/widgets/form.julius
rename to static/js/utils/form.js
index e318ca3f3..30ba76c96 100644
--- a/templates/widgets/form.julius
+++ b/static/js/utils/form.js
@@ -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
(array) and
- // enables