Merge branch 'master' into feat/pagination

This commit is contained in:
Gregor Kleen 2018-06-27 08:58:29 +02:00
commit 936b808aaf
62 changed files with 3535 additions and 813 deletions

View File

@ -1,4 +1,4 @@
SummerTerm year@Integer: Sommersemester #{tshow year}
SummerTerm year@Integer: Sommersemester #{tshow year}
WinterTerm year@Integer: Wintersemester #{tshow year}/#{tshow $ succ year}
PSLimitNonPositive: “pagesize” muss größer als null sein
Page n@Int64: #{tshow n}
@ -9,16 +9,18 @@ Term: Semester
TermPlaceholder: W/S + vierstellige Jahreszahl
TermEditHeading: Semester editieren/anlegen
Course: Kurs
CourseNewOk tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} wurde erfolgreich erstellt.
CourseEditOk tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} wurde erfolgreich geändert.
CourseNewDupShort tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester.
CourseEditDupShort tid@TermIdentifier courseShortHand@Text: Kurs #{termToText tid}-#{courseShortHand} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester.
FFSheetName: Name
TermCourseListHeading tid@TermIdentifier: Kursübersicht #{termToText tid}
TermCourseListHeading tid@TermIdentifier: Kursübersicht #{termToText tid}
TermCourseListTitle tid@TermIdentifier: Kurse #{termToText tid}
CourseEditHeading: Kurs editieren/anlegen
CourseEditTitle: Kurs editieren/anlegen
Sheet: Blatt
SheetNewOk tid@TermIdentifier courseShortHand@Text sheetName@Text: Neues Übungsblatt #{sheetName} wurde im Kurs #{termToText tid}-#{courseShortHand} erfolgreich erstellt.
SheetTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand} #{sheetName}
SheetTitleNew tid@TermIdentifier courseShortHand@Text : #{termToText tid}-#{courseShortHand}: Neues Übungsblatt
@ -28,9 +30,12 @@ SheetDelTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: Übun
SheetDelText submissionNo@Int: Dies kann nicht mehr rückgängig gemacht werden! Alle Einreichungen gehen ebenfalls verloren! Es gibt #{show submissionNo} Abgaben.
SheetDelOk tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termToText tid}-#{courseShortHand}: Übungsblatt #{sheetName} gelöscht.
Deadline: Abgabe
Done: Eingereicht
Unauthorized: Sie haben hierfür keine explizite Berechtigung.
UnauthorizedAnd l@Text r@Text: "#{l}" und "#{r}"
UnauthorizedOr l@Text r@Text: "#{l}" oder "#{r}"
UnauthorizedAnd l@Text r@Text: #{l} UND #{r}
UnauthorizedOr l@Text r@Text: #{l} ODER #{r}
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen.
UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter für dieses Institut eingetragen.
UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung eingetragen.
@ -52,13 +57,15 @@ SubmissionTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: #{termTo
SubmissionMember g@Int: Mitabgebende(r) ##{tshow g}
SubmissionArchive: Zip-Archiv der Abgabedatei(en)
SubmissionFile: Datei zur Abgabe
SubmissionAlreadyExistsFor user@Text: #{user} hat bereits eine Abgabe zu diesem Übungsblatt.
SubmissionAlreadyExistsFor user@Text: #{user} hat bereits eine Abgabe zu diesem bÜbungsblatt.
EMail: E-Mail
EMailUnknown email@Text: E-Mail #{email} gehört zu keinem bekannten Benutzer.
NotAParticipant user@Text tid@TermIdentifier csh@Text: #{user} ist nicht im Kurs #{termToText tid}-#{csh} angemeldet.
HomeHeading: Startseite
HomeHeading: Aktuelle Termine
ProfileHeading: Benutzerprofil und Einstellungen
ProfileDataHeading: Gespeicherte Benutzerdaten
TermsHeading: Semesterübersicht
NumCourses n@Int64: #{tshow n} Kurse
@ -70,3 +77,12 @@ Theme: Oberflächen Design
Favoriten: Anzahl gespeicherter Favoriten
Plugin: Plugin
Ident: Identifizierung
Settings: Individuelle Benutzereinstellungen
SettingsUpdate: Einstellungen wurden gespeichert.
SheetExercise: Aufgabenstellung
SheetHint: Hinweise
SheetSolution: Lösung
SheetMarking: Korrekturhinweise
MultiFileUploadInfo: (Mehrere Dateien mit Shift oder Strg auswählen)

31
routes
View File

@ -30,15 +30,18 @@
/favicon.ico FaviconR GET !free
/robots.txt RobotsR GET !free
/ HomeR GET POST !free
/profile ProfileR GET !free
/users UsersR GET -- no tags, i.e. admins only
/ HomeR GET !free
/users UsersR GET -- no tags, i.e. admins only
/admin/test AdminTestR GET POST
/terms TermShowR GET !free
/terms/current TermCurrentR GET !free
/terms/edit TermEditR GET POST
/terms/#TermId/edit TermEditExistR GET
!/terms/#TermId TermCourseListR GET !free
/profile ProfileR GET POST !free !free
/profile/data ProfileDataR GET !free !free
/terms TermShowR GET !free
/terms/current TermCurrentR GET !free
/terms/edit TermEditR GET POST
/terms/#TermId/edit TermEditExistR GET
!/terms/#TermId TermCourseListR GET !free
-- For Pattern Synonyms see Foundation
/course/ CourseListR GET !free
@ -50,20 +53,22 @@
!/ex/new SheetNewR GET POST
/ex/#Text SheetR:
/show SShowR GET !timeANDregistered !timeANDmaterials !corrector
/#SheetFileType/#FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector
!/#SheetFileType/*FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector
/edit SEditR GET POST
/delete SDelR GET POST
!/sub/new SubmissionNewR GET POST !timeANDregistered
!/sub/own SubmissionOwnR GET !free
!/sub/#CryptoUUIDSubmission SubmissionR GET POST !owner !corrector
!/sub/#CryptoFileNameSubmission SubmissionR GET POST !owner !corrector
!/#UUID CryptoUUIDDispatchR GET !free -- just redirect
-- TODO below
!/#{ZIPArchiveName SubmissionId} SubmissionDownloadArchiveR GET !deprecated
!/#CryptoUUIDSubmission/#FilePath SubmissionDownloadSingleR GET !deprecated
!/#CryptoFileNameSubmission/*FilePath SubmissionDownloadSingleR GET !deprecated
/submission SubmissionListR GET !deprecated
/submission/#CryptoUUIDSubmission SubmissionDemoR GET POST !deprecated
/submissions.zip SubmissionDownloadMultiArchiveR POST !deprecated
-- TODO above
!/#UUID CryptoUUIDDispatchR GET !free -- just redirect
!/*{CI FilePath} CryptoFileNameDispatchR GET !free

View File

@ -43,6 +43,7 @@ import Handler.Common
import Handler.Home
import Handler.Profile
import Handler.Users
import Handler.Admin
import Handler.Term
import Handler.Course
import Handler.Sheet

View File

@ -24,6 +24,8 @@ import Data.CryptoID.Poly.ImplicitNamespace
import Data.UUID.Cryptographic.ImplicitNamespace
import System.FilePath.Cryptographic.ImplicitNamespace
import qualified Data.Text as Text
import Data.UUID.Types
import Web.PathPieces
@ -35,24 +37,33 @@ instance PathPiece UUID where
fromPathPiece = fromString . unpack
toPathPiece = pack . toString
instance (CI.FoldCase s, PathPiece s) => PathPiece (CI s) where
fromPathPiece = fmap CI.mk . fromPathPiece
toPathPiece = toPathPiece . CI.original
-- Generates CryptoUUID... Datatypes
instance {-# OVERLAPS #-} PathMultiPiece FilePath where
fromPathMultiPiece = Just . unpack . intercalate "/"
toPathMultiPiece = Text.splitOn "/" . pack
instance (CI.FoldCase s, PathMultiPiece s) => PathMultiPiece (CI s) where
fromPathMultiPiece = fmap CI.mk . fromPathMultiPiece
toPathMultiPiece = toPathMultiPiece . CI.original
-- Generates CryptoUUID... and CryptoFileName... Datatypes
decCryptoIDs [ ''SubmissionId
, ''CourseId
, ''SheetId
, ''FileId
, ''UserId
]
{- TODO: Do we need/want CryptoUUIDs for Sheet numbers? -}
newtype SubmissionMode = SubmissionMode (Maybe CryptoUUIDSubmission)
newtype SubmissionMode = SubmissionMode (Maybe CryptoFileNameSubmission)
deriving (Show, Read, Eq)
pattern NewSubmission :: SubmissionMode
pattern NewSubmission = SubmissionMode Nothing
pattern ExistingSubmission :: CryptoUUIDSubmission -> SubmissionMode
pattern ExistingSubmission :: CryptoFileNameSubmission -> SubmissionMode
pattern ExistingSubmission cID = SubmissionMode (Just cID)
instance PathPiece SubmissionMode where
@ -62,6 +73,7 @@ instance PathPiece SubmissionMode where
toPathPiece (SubmissionMode Nothing) = "new"
toPathPiece (SubmissionMode (Just x)) = toPathPiece x
newtype ZIPArchiveName objID = ZIPArchiveName (CryptoID (CI FilePath) objID)
deriving (Show, Read, Eq)

View File

@ -157,6 +157,13 @@ instance RenderMessage UniWorX TermIdentifier where
instance RenderMessage UniWorX String where
renderMessage f ls str = renderMessage f ls $ Text.pack str
instance RenderMessage UniWorX SheetFileType where
renderMessage foundation ls = \case
SheetExercise -> renderMessage' MsgSheetExercise
SheetHint -> renderMessage' MsgSheetHint
SheetSolution -> renderMessage' MsgSheetSolution
SheetMarking -> renderMessage' MsgSheetMarking
where renderMessage' = renderMessage foundation ls
-- Access Control
data AccessPredicate
@ -278,13 +285,14 @@ knownTags = -- should not throw exceptions, i.e. no getBy404 or requireAuthId
Entity cid _ <- MaybeT . getBy $ CourseTermShort tid csh
Entity sid Sheet{..} <- MaybeT . getBy $ CourseSheet cid shn
cTime <- liftIO getCurrentTime
let started = sheetActiveFrom <= cTime || NTop sheetVisibleFrom <= (NTop $ Just cTime)
case subRoute of
SFileR SheetExercise _ -> guard $ maybe False (<= cTime) sheetVisibleFrom
SFileR SheetExercise _ -> guard started
SFileR SheetHint _ -> guard $ maybe False (<= cTime) sheetHintFrom
SFileR SheetSolution _ -> guard $ maybe False (<= cTime) sheetSolutionFrom
SFileR SheetMarking _ -> mzero -- only for correctors and lecturers
SubmissionNewR -> guard $ sheetActiveFrom <= cTime && cTime <= sheetActiveTo
_ -> guard $ maybe False (<= cTime) sheetVisibleFrom
SubmissionNewR -> guard $ sheetActiveFrom <= cTime && cTime <= sheetActiveTo
_ -> guard started
return Authorized
r -> do
$logErrorS "AccessControl" $ "'!time' used on route that doesn't support it: " <> tshow r
@ -412,7 +420,7 @@ instance Yesod UniWorX where
[ Desc CourseFavouriteTime
, OffsetBy $ userMaxFavourites user
]
lift $ mapM delete oldFavs
lift $ mapM_ delete oldFavs
_other -> return ()
return res
@ -496,10 +504,12 @@ instance Yesod UniWorX where
addStylesheet $ StaticR css_tabber_css
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_icons_css
addStylesheet $ StaticR css_fontawesome_css
$(widgetFile "default-layout")
$(widgetFile "standalone/modal")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/inputs")
$(widgetFile "standalone/tooltip")
$(widgetFile "standalone/tabber")
$(widgetFile "standalone/alerts")
$(widgetFile "standalone/datepicker")
@ -573,9 +583,10 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
breadcrumb HomeR = return ("Uniworky", Nothing)
breadcrumb HomeR = return ("UniWorkY", Nothing)
breadcrumb (AuthR _) = return ("Login", Just HomeR)
breadcrumb ProfileR = return ("Profile", Just HomeR)
breadcrumb ProfileDataR = return ("Data", Just ProfileR)
breadcrumb _ = return ("home", Nothing)
pageActions :: Route UniWorX -> [MenuTypes]
@ -631,6 +642,30 @@ pageActions (TermCourseListR _) =
, menuItemAccessCallback' = return True
}
]
pageActions (ProfileR) =
[ PageActionPrime $ MenuItem
{ menuItemLabel = "Gespeicherte Daten anzeigen"
, menuItemIcon = Just "book"
, menuItemRoute = ProfileDataR
, menuItemAccessCallback' = return True
}
]
pageActions (HomeR) =
[
-- NavbarAside $ MenuItem
-- { menuItemLabel = "Benutzer"
-- , menuItemIcon = Just "users"
-- , menuItemRoute = UsersR
-- , menuItemAccessCallback' = return True
-- }
-- ,
NavbarAside $ MenuItem
{ menuItemLabel = "AdminDemo"
, menuItemIcon = Nothing
, menuItemRoute = AdminTestR
, menuItemAccessCallback' = return True
}
]
pageActions _ = []
@ -640,6 +675,12 @@ i18nHeading msg = liftWidgetT $ toWidget =<< getMessageRender <*> pure msg
pageHeading :: Route UniWorX -> Maybe Widget
pageHeading HomeR
= Just $ i18nHeading MsgHomeHeading
pageHeading (AdminTestR)
= Just $ [whamlet|Internal Code Demonstration Page|]
pageHeading ProfileR
= Just $ i18nHeading MsgProfileHeading
pageHeading ProfileDataR
= Just $ i18nHeading MsgProfileDataHeading
pageHeading TermShowR
= Just $ i18nHeading MsgTermsHeading
pageHeading TermEditR
@ -683,20 +724,20 @@ defaultLinks = -- Define the menu items of the header.
, menuItemAccessCallback' = isJust <$> maybeAuthPair
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Veranstaltungen"
, menuItemIcon = Just "book"
{ menuItemLabel = "Kurse"
, menuItemIcon = Just "calendar-alt"
, menuItemRoute = CourseListR -- should be CourseListActiveR or similar in the future
, menuItemAccessCallback' = return True
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Semester"
, menuItemIcon = Nothing
, menuItemIcon = Just "graduation-cap"
, menuItemRoute = TermShowR
, menuItemAccessCallback' = return True
}
, NavbarAside $ MenuItem
{ menuItemLabel = "Benutzer"
, menuItemIcon = Just "user"
, menuItemIcon = Just "users"
, menuItemRoute = UsersR
, menuItemAccessCallback' = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
}

60
src/Handler/Admin.hs Normal file
View File

@ -0,0 +1,60 @@
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
module Handler.Admin where
import Import
import Handler.Utils
-- import Data.Time
-- import qualified Data.Text as T
-- import Data.Function ((&))
-- import Yesod.Form.Bootstrap3
import Web.PathPieces (showToPathPiece, readFromPathPiece)
-- import Colonnade hiding (fromMaybe)
-- import Yesod.Colonnade
-- import qualified Data.UUID.Cryptographic as UUID
-- BEGIN - Buttons needed only here
data CreateButton = CreateMath | CreateInf -- Dummy for Example
deriving (Enum, Eq, Ord, Bounded, Read, Show)
instance PathPiece CreateButton where -- for displaying the button only, not really for paths
toPathPiece = showToPathPiece
fromPathPiece = readFromPathPiece
instance Button CreateButton where
label CreateMath = [whamlet|Ma<i>thema</i>tik|]
label CreateInf = "Informatik"
cssClass CreateMath = BCInfo
cssClass CreateInf = BCPrimary
-- END Button needed here
getAdminTestR :: Handler Html -- Demo Page. Referenzimplementierungen sollte hier gezeigt werden!
getAdminTestR = do
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton)
defaultLayout $ do
-- setTitle "UniWorkY Admin Testpage"
$(widgetFile "adminTest")
postAdminTestR :: Handler Html
postAdminTestR = do
((btnResult,_), _) <- runFormPost $ buttonForm
case btnResult of
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
(FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt"
_other -> return ()
getAdminTestR

View File

@ -142,12 +142,6 @@ postCEditR tid csh = do
course <- runDB $ getBy $ CourseTermShort tid csh
courseEditHandler False course
getCourseEditIDR :: CryptoUUIDCourse -> Handler Html
getCourseEditIDR cID = do
cIDKey <- getsYesod appCryptoIDKey
courseID <- UUID.decrypt cIDKey cID
courseEditHandler True =<< runDB (getEntity courseID)
courseDeleteHandler :: Handler Html -- not called anywhere yet
courseDeleteHandler = undefined

View File

@ -14,6 +14,7 @@
module Handler.CryptoIDDispatch
( getCryptoUUIDDispatchR
, getCryptoFileNameDispatchR
) where
import Import hiding (Proxy)
@ -26,11 +27,25 @@ import Yesod.Core.Types (HandlerContents(..), ErrorResponse(..))
import qualified Control.Monad.Catch as E (Handler(..))
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
class CryptoRoute ciphertext plaintext where
cryptoIDRoute :: p plaintext -> ciphertext -> Handler (Route UniWorX)
instance CryptoRoute UUID SubmissionId where
cryptoIDRoute _ (CryptoID -> cID) = do
(smid :: SubmissionId) <- decrypt cID
cID' <- encrypt smid
(tid,csh,shn) <- runDB $ do
shid <- submissionSheet <$> get404 smid
Sheet{..} <- get404 shid
Course{..} <- get404 sheetCourse
return (courseTerm, courseShorthand, sheetName)
return $ CSheetR tid csh shn $ SubmissionR cID'
instance CryptoRoute (CI FilePath) SubmissionId where
cryptoIDRoute _ (CryptoID -> cID) = do
(smid :: SubmissionId) <- decrypt cID
(tid,csh,shn) <- runDB $ do
@ -39,7 +54,7 @@ instance CryptoRoute UUID SubmissionId where
Course{..} <- get404 sheetCourse
return (courseTerm, courseShorthand, sheetName)
return $ CSheetR tid csh shn $ SubmissionR cID
class Dispatch ciphertext (x :: [*]) where
dispatchID :: p x -> ciphertext -> Handler (Maybe (Route UniWorX))
@ -66,3 +81,9 @@ getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectWith
p :: Proxy '[ SubmissionId
]
p = Proxy
getCryptoFileNameDispatchR :: CI FilePath -> Handler ()
getCryptoFileNameDispatchR path = dispatchID p path >>= maybe notFound (redirectWith found302)
where
p :: Proxy '[ SubmissionId ]
p = Proxy

View File

@ -1,57 +1,171 @@
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE IncoherentInstances #-} -- why is this needed? Instance for "display deadline" ought to be clear
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PartialTypeSignatures #-}
module Handler.Home where
import Import
import Handler.Utils
-- import Data.Time
import Data.Time
-- import qualified Data.Text as T
-- import Yesod.Form.Bootstrap3
import Web.PathPieces (showToPathPiece, readFromPathPiece)
-- import Web.PathPieces (showToPathPiece, readFromPathPiece)
-- import Colonnade
-- import Control.Lens
import Colonnade hiding (fromMaybe, singleton)
-- import Yesod.Colonnade
import qualified Database.Esqueleto as E
-- import qualified Data.UUID.Cryptographic as UUID
-- BEGIN - Buttons needed only here
data CreateButton = CreateMath | CreateInf -- Dummy for Example
deriving (Enum, Eq, Ord, Bounded, Read, Show)
instance PathPiece CreateButton where -- for displaying the button only, not really for paths
toPathPiece = showToPathPiece
fromPathPiece = readFromPathPiece
-- Some constants:
nrSheetDeadlines :: Int64
nrSheetDeadlines = 10
offSheetDeadlines :: NominalDiffTime
offSheetDeadlines = 15
--nrExamDeadlines = 10
--offExamDeadlines = 15
--nrCourseDeadlines = 10
--offCourseDeadlines = 15
instance Button CreateButton where
label CreateMath = [whamlet|Ma<i>thema</i>tik|]
label CreateInf = "Informatik"
cssClass CreateMath = BCInfo
cssClass CreateInf = BCPrimary
-- END Button needed here
getHomeR :: Handler Html
getHomeR = do
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form CreateButton)
muid <- maybeAuthId
case muid of
Nothing -> homeAnonymous
Just uid -> homeUser uid
homeAnonymous :: Handler Html
homeAnonymous = do
cTime <- liftIO getCurrentTime
let fTime = addUTCTime (offSheetDeadlines * nominalDay) cTime
let tableData :: E.SqlExpr (Entity Course)
-> E.SqlQuery (E.SqlExpr (Entity Course))
tableData course = do
E.where_ $ course E.^. CourseHasRegistration E.==. E.val True
E.&&. course E.^. CourseRegisterFrom E.<=. E.val (Just cTime)
E.&&. ((E.isNothing $ course E.^. CourseRegisterTo)
E.||. (course E.^. CourseRegisterTo E.>=. E.val (Just cTime)))
E.limit nrSheetDeadlines
E.orderBy [ E.asc $ course E.^. CourseRegisterTo
, E.desc $ course E.^. CourseShorthand
]
E.limit nrSheetDeadlines
return course
colonnade :: Colonnade Sortable (DBRow (Entity Course)) (DBCell (WidgetT UniWorX IO) ())
colonnade = mconcat
[ -- dbRow
sortable (Just "course") (textCell MsgCourse) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> do
let tid = courseTerm course
csh = courseShorthand course
cell [whamlet|<a href=@{CourseR tid csh CShowR}>#{display csh}|]
, sortable (Just "deadline") (textCell MsgDeadline) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } ->
textCell $ display $ courseRegisterTo course
]
courseTable <- dbTable def $ DBTable
{ dbtSQLQuery = tableData
, dbtColonnade = colonnade
, dbtSorting = [ ( "term"
, SortColumn $ \(course) -> course E.^. CourseTerm
)
, ( "course"
, SortColumn $ \(course) -> course E.^. CourseShorthand
)
-- TODO
]
, dbtFilter = mempty {- [ ( "term"
, FilterColumn $ \(course `E.InnerJoin` _ `E.InnerJoin` _ ) tids -> if
| Set.null tids -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> course E.^. CourseTerm `E.in_` E.valList (Set.toList tids)
)
] -}
, dbtAttrs = tableDefault
, dbtIdent = "upcomingdeadlines" :: Text
}
defaultLayout $ do
setTitle "Willkommen zum Uniworky Test!"
$(widgetFile "dsgvDisclaimer")
$(widgetFile "home")
homeUser :: Key User -> Handler Html
homeUser uid = do
cTime <- liftIO getCurrentTime
let fTime = addUTCTime (offSheetDeadlines * nominalDay) cTime
postHomeR :: Handler Html
postHomeR = do
((btnResult,_), _) <- runFormPost $ buttonForm
case btnResult of
(FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt"
(FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt"
_other -> return ()
getHomeR
tableData :: E.InnerJoin (E.InnerJoin (E.SqlExpr (Entity CourseParticipant))
(E.SqlExpr (Entity Course )))
(E.SqlExpr (Entity Sheet ))
-> E.SqlQuery ( E.SqlExpr (E.Value (Key Term))
, E.SqlExpr (E.Value Text)
, E.SqlExpr (E.Value Text)
, E.SqlExpr (E.Value UTCTime))
tableData (participant `E.InnerJoin` course `E.InnerJoin` sheet) = do
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
E.where_ $ participant E.^. CourseParticipantUser E.==. E.val uid
E.&&. sheet E.^. SheetActiveTo E.<=. E.val fTime
E.&&. sheet E.^. SheetActiveTo E.>=. E.val cTime
E.orderBy [ E.asc $ sheet E.^. SheetActiveTo
, E.desc $ sheet E.^. SheetName
, E.desc $ course E.^. CourseShorthand
]
E.limit nrSheetDeadlines
return
( course E.^. CourseTerm
, course E.^. CourseShorthand
, sheet E.^. SheetName
, sheet E.^. SheetActiveTo
)
colonnade :: Colonnade Sortable (DBRow (E.Value (Key Term), E.Value Text, E.Value Text, E.Value UTCTime)) (DBCell (WidgetT UniWorX IO) ())
colonnade = mconcat
[ -- dbRow
sortable (Just "course") (textCell MsgCourse) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, _, _) } ->
cell [whamlet|<a href=@{CourseR tid csh CShowR}>#{display csh}|]
, sortable (Just "sheet") (textCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _) } ->
cell [whamlet|<a href=@{CSheetR tid csh shn SShowR}>#{display shn}|]
, sortable (Just "deadline") (textCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, E.Value deadline) } ->
textCell $ display deadline
, sortable (Just "done") (textCell MsgDone) $ \DBRow{ dbrOutput=(_, _, _, _) } ->
textCell ("?" :: Text)
]
sheetTable <- dbTable def $ DBTable
{ dbtSQLQuery = tableData
, dbtColonnade = colonnade
, dbtSorting = [ ( "term"
, SortColumn $ \(_ `E.InnerJoin` course `E.InnerJoin` _ ) -> course E.^. CourseTerm
)
, ( "course"
, SortColumn $ \(_ `E.InnerJoin` course `E.InnerJoin` _ ) -> course E.^. CourseShorthand
)
-- TODO
]
, dbtFilter = mempty {- [ ( "term"
, FilterColumn $ \(course `E.InnerJoin` _ `E.InnerJoin` _ ) tids -> if
| Set.null tids -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> course E.^. CourseTerm `E.in_` E.valList (Set.toList tids)
)
] -}
, dbtAttrs = tableDefault
, dbtIdent = "upcomingdeadlines" :: Text
}
defaultLayout $ do
-- setTitle "Willkommen zum Uniworky Test!"
$(widgetFile "homeUser")
$(widgetFile "dsgvDisclaimer")

View File

@ -1,6 +1,7 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
@ -10,27 +11,70 @@ import Import
import Handler.Utils
import Colonnade hiding (fromMaybe, singleton)
import Yesod.Colonnade
-- import Colonnade hiding (fromMaybe, singleton)
-- import Yesod.Colonnade
import qualified Database.Esqueleto as E
import Database.Esqueleto ((^.))
data SettingsForm = SettingsForm
{ stgMaxFavourties :: Int
, stgTheme :: Theme
}
makeSettingForm :: Maybe SettingsForm -> Form SettingsForm
makeSettingForm template = identForm FIDsettings $ \html -> do
let themeList = [(display t,t) | t <- allThemes]
(result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm
<$> areq (natField "Favoriten") -- TODO: natFieldI not working here
(fslpI MsgFavoriten "Anzahl Favoriten") (stgMaxFavourties <$> template)
<*> areq (selectFieldList themeList)
(fslI MsgTheme ) (stgTheme <$> template)
<* submitButton
return (result, widget) -- no validation required here
getProfileR :: Handler Html
getProfileR = do
(uid, User{..}) <- requireAuthPair
mr <- getMessageRender
(admin_rights,lecturer_rights,lecture_owner,lecture_corrector,participant,studies) <- runDB $ (,,,,,) <$>
(uid, User{..}) <- requireAuthPair
let settingsTemplate = Just $ SettingsForm
{ stgMaxFavourties = userMaxFavourites
, stgTheme = userTheme
}
((res,formWidget), formEnctype) <- runFormPost $ makeSettingForm settingsTemplate
case res of
(FormSuccess SettingsForm{..}) -> do
runDB $ do
update uid [ UserMaxFavourites =. stgMaxFavourties
, UserTheme =. stgTheme
]
when (stgMaxFavourties < userMaxFavourites) $ do
-- prune Favourites to user-defined size
oldFavs <- selectKeysList [ CourseFavouriteUser ==. uid]
[ Desc CourseFavouriteTime
, OffsetBy $ stgMaxFavourties
]
mapM_ delete oldFavs
addMessageI "info" $ MsgSettingsUpdate
(FormFailure msgs) -> forM_ msgs $ (addMessage "warning") . toHtml
_ -> return ()
(admin_rights,lecturer_rights,lecture_owner,lecture_corrector,participant,studies) <- runDB $ (,,,,,) <$>
(E.select $ E.from $ \(adright `E.InnerJoin` school) -> do
E.where_ $ adright ^. UserAdminUser E.==. E.val uid
E.on $ adright ^. UserAdminSchool E.==. school ^. SchoolId
return (school ^. SchoolName)
return (school ^. SchoolShorthand)
)
<*>
(E.select $ E.from $ \(lecright `E.InnerJoin` school) -> do
E.where_ $ lecright ^. UserLecturerUser E.==. E.val uid
E.on $ lecright ^. UserLecturerSchool E.==. school ^. SchoolId
return (school ^. SchoolName)
return (school ^. SchoolShorthand)
)
<*>
(E.select $ E.from $ \(lecturer `E.InnerJoin` course) -> do
@ -60,21 +104,25 @@ getProfileR = do
,studyfeat ^. StudyFeaturesType
,studyfeat ^. StudyFeaturesSemester)
)
let formText = Just MsgSettings
actionUrl = ProfileR
settingsForm = $(widgetFile "formPageI18n")
defaultLayout $ do
setTitle . toHtml $ userIdent <> "'s User page"
$(widgetFile "profile")
$(widgetFile "dsgvDisclaimer")
let userData =
[ (MsgName , userDisplayName )
, (MsgIdent , userIdent )
, (MsgPlugin , userPlugin )
, (MsgMatrikelNr , display userMatrikelnummer)
, (MsgEMail , userEmail )
, (MsgFavoriten , display userMaxFavourites)
, (MsgTheme , display userTheme )
]
userDisplay = mconcat
[ headless $ toWgt . mr . fst
, headless $ toWgt . snd
] --TODO Continue here!!!
userTable = encodeWidgetTable tableDefault userDisplay userData
defaultLayout $ do
setTitle . toHtml $ userIdent <> "'s User page"
$(widgetFile "profile")
postProfileR :: Handler Html
postProfileR = do
-- TODO
getProfileR
getProfileDataR :: Handler Html
getProfileDataR = do
(uid, User{..}) <- requireAuthPair
-- mr <- getMessageRender
defaultLayout $ do
$(widgetFile "profileData")
$(widgetFile "dsgvDisclaimer")

View File

@ -161,7 +161,7 @@ getSheetList courseEnt = do
[ headed "Blatt" $ \(sid,sheet,_) -> simpleLink (toWgt $ sheetName sheet) $ CSheetR tid csh (sheetName sheet) SShowR
, headed "Abgabe ab" $ toWgt . formatTimeGerWD . sheetActiveFrom . snd3
, headed "Abgabe bis" $ toWgt . formatTimeGerWD . sheetActiveTo . snd3
, headed "Bewertung" $ toWgt . show . sheetType . snd3
, headed "Bewertung" $ toWgt . display . sheetType . snd3
]
let colAdmin = mconcat -- only show edit button for allowed course assistants
[ headed "Korrigiert" $ toWgt . snd . trd3
@ -212,19 +212,17 @@ getSShowR tid csh shn = do
E.&&. E.not_ (E.isNothing $ file E.^. FileContent)
-- return desired columns
return $ (file E.^. FileTitle, file E.^. FileModified, sheetFile E.^. SheetFileType)
let
colonnadeFiles :: Colonnade Sortable _ (DBCell (WidgetT UniWorX IO) ())
colonnadeFiles = mconcat
[ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> textCell $ toPathPiece ftype
, sortable (Just "path") "Dateiname" $ anchorCell (\(E.Value fName,_,E.Value fType) -> CSheetR tid csh shn (SFileR fType fName))
(\(E.Value fName,_,_) -> str2widget fName)
, sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> stringCell $ formatTimeGerWDT (modified :: UTCTime)
]
let colonnadeFiles = widgetColonnade $ mconcat
[ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> stringCell ftype
, sortable (Just "path") "Dateiname" $ anchorCell (\(E.Value fName,_,E.Value fType) -> CSheetR tid csh shn (SFileR fType fName))
(\(E.Value fName,_,_) -> str2widget fName)
, sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> stringCell $ formatTimeGerWDT (modified :: UTCTime)
]
fileTable <- dbTable def $ DBTable
{ dbtSQLQuery = fileData
, dbtColonnade = colonnadeFiles
, dbtAttrs = tableDefault
-- , dbtFilter = Map.empty -- TODO: Just for Testing. Gregor needs to explain what is needed here.
, dbtFilter = Map.empty
, dbtIdent = "files" :: Text
-- TODO: Add column for and visibility date
, dbtSorting = [ ( "type"

View File

@ -62,7 +62,7 @@ makeSubmissionForm :: Maybe SubmissionId -> Bool -> SheetGroup -> [Text] -> Form
makeSubmissionForm msmid unpackZips grouping buddies = identForm FIDsubmission $ \html -> do
flip (renderAForm FormStandard) html $ (,)
<$> (bool (\f fs _ -> Just <$> areq f fs Nothing) aopt $ isJust msmid) (zipFileField unpackZips) (fsm $ bool MsgSubmissionFile MsgSubmissionArchive unpackZips) Nothing
<*> (catMaybes <$> sequenceA [bool aforced' aopt editableBuddies textField (fsm $ MsgSubmissionMember g) buddy
<*> (catMaybes <$> sequenceA [bool aforced' aopt editableBuddies textField (fslpI (MsgSubmissionMember g) "user@campus.lmu.de" ) buddy
| g <- [1..(max groupNr $ length buddies)] -- groupNr might have decreased meanwhile
| buddy <- map (Just . Just) buddies ++ repeat Nothing -- show current buddies
])
@ -80,7 +80,7 @@ getSubmissionNewR = postSubmissionNewR
postSubmissionNewR tid csh shn = submissionHelper tid csh shn NewSubmission
getSubmissionR, postSubmissionR :: TermId -> Text -> Text -> CryptoUUIDSubmission -> Handler Html
getSubmissionR, postSubmissionR :: TermId -> Text -> Text -> CryptoFileNameSubmission -> Handler Html
getSubmissionR = postSubmissionR
postSubmissionR tid csh shn cid = submissionHelper tid csh shn $ ExistingSubmission cid
@ -291,16 +291,15 @@ submissionFileQuery submissionID (sf `E.InnerJoin` f) = E.distinctOnOrderBy [E.a
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] -- E.desc returns corrector updated data first
return f
getSubmissionDownloadSingleR :: CryptoUUIDSubmission -> FilePath -> Handler TypedContent
getSubmissionDownloadSingleR :: CryptoFileNameSubmission -> FilePath -> Handler TypedContent
getSubmissionDownloadSingleR cID path = do
submissionID <- decrypt cID
cID' <- encrypt submissionID
runDB $ do
isRating <- maybe False (== submissionID) <$> isRatingFile path
case isRating of
True -> do
file <- runMaybeT $ lift . ratingFile cID' =<< MaybeT (getRating submissionID)
file <- runMaybeT $ lift . ratingFile cID =<< MaybeT (getRating submissionID)
maybe notFound (return . toTypedContent . Text.decodeUtf8) $ fileContent =<< file
False -> do
results <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do

View File

@ -87,6 +87,13 @@ getTermShowR = do
, dbtFilter = [ ( "active"
, FilterColumn $ \term -> (term E.^. TermActive :: E.SqlExpr (E.Value Bool))
)
, ( "course"
, FilterColumn $ \term csh -> case csh of -- FilterColumn-Lambdas are
[] -> E.val True :: E.SqlExpr (E.Value Bool)
cshs -> E.exists . E.from $ \course -> do
E.where_ $ course E.^. CourseTerm E.==. term E.^. TermId
E.&&. course E.^. CourseShorthand `E.in_` E.valList cshs
)
]
, dbtAttrs = tableDefault
, dbtIdent = "terms" :: Text

View File

@ -14,7 +14,7 @@
module Handler.Utils.Form where
import Handler.Utils.Form.Types
import Handler.Utils.Templates
import Handler.Utils.DateTime
@ -48,7 +48,7 @@ import Control.Monad.Writer.Class
-- Unique Form Identifiers to avoid accidents --
------------------------------------------------
data FormIdentifier = FIDcourse | FIDsheet | FIDsubmission
data FormIdentifier = FIDcourse | FIDsheet | FIDsubmission | FIDsettings
deriving (Enum, Eq, Ord, Bounded, Read, Show)
@ -226,6 +226,10 @@ buttonForm csrf = do
-- Fields --
------------
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => FormMessage -> Field m i
natFieldI msg = checkBool (>= 0) msg intField
natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
natField d = checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") $ intField
@ -366,7 +370,7 @@ utcTimeField = Field
where
fieldTimeFormat :: String
--fieldTimeFormat = "%e.%m.%y %k:%M"
fieldTimeFormat = "%Y-%m-%eT%H:%M"
fieldTimeFormat = "%Y-%m-%dT%H:%M"
readTime :: Text -> Either FormMessage UTCTime
readTime t =
@ -385,8 +389,17 @@ fsb :: Text -> FieldSettings site -- DEPRECATED
fsb = bfs -- Just to avoid annoying Ambiguous Type Errors
fsl :: Text -> FieldSettings UniWorX
fsl label =
FieldSettings { fsLabel = (SomeMessage label)
fsl lbl =
FieldSettings { fsLabel = (SomeMessage lbl)
, fsTooltip = Nothing
, fsId = Nothing
, fsName = Nothing
, fsAttrs = []
}
fslI :: RenderMessage UniWorX msg => msg -> FieldSettings UniWorX
fslI lbl =
FieldSettings { fsLabel = (SomeMessage lbl)
, fsTooltip = Nothing
, fsId = Nothing
, fsName = Nothing
@ -394,8 +407,8 @@ fsl label =
}
fslp :: Text -> Text -> FieldSettings UniWorX
fslp label placeholder =
FieldSettings { fsLabel = (SomeMessage label)
fslp lbl placeholder =
FieldSettings { fsLabel = (SomeMessage lbl)
, fsTooltip = Nothing
, fsId = Nothing
, fsName = Nothing
@ -403,8 +416,8 @@ fslp label placeholder =
}
fslpI :: RenderMessage UniWorX msg => msg -> Text -> FieldSettings UniWorX
fslpI label placeholder =
FieldSettings { fsLabel = (SomeMessage label)
fslpI lbl placeholder =
FieldSettings { fsLabel = (SomeMessage lbl)
, fsTooltip = Nothing
, fsId = Nothing
, fsName = Nothing

View File

@ -35,6 +35,9 @@ numberColonnade = headed "Nr" (fromString.show)
pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c
pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
i18nCell :: RenderMessage site a => a -> Cell site
i18nCell msg = cell [whamlet|_{msg}|]
-- Table Modification
encodeHeadedWidgetTableNumbered :: Attribute -> Colonnade Headed a (WidgetT site IO ()) -> [a] -> WidgetT site IO ()

View File

@ -43,6 +43,9 @@ type Points = Centi
toPoints :: Integral a => a -> Points
toPoints = MkFixed . fromIntegral
pToI :: Points -> Integer
pToI = fromPoints -- TODO: do we want to multiply?
fromPoints :: Integral a => Points -> a
fromPoints (MkFixed c) = fromInteger c
@ -52,6 +55,13 @@ data SheetType
| Pass { maxPoints, passingPoints :: Points }
| NotGraded
deriving (Show, Read, Eq)
instance DisplayAble SheetType where
display (Bonus {..}) = tshow (pToI maxPoints) <> " Bonuspunkte"
display (Normal{..}) = tshow (pToI maxPoints) <> " Punkte"
display (Pass {..}) = "Bestanden ab " <> tshow (pToI passingPoints) <> " von " <> tshow (pToI maxPoints)
display (NotGraded) = "Unbewertet"
deriveJSON defaultOptions ''SheetType
derivePersistFieldJSON "SheetType"
@ -75,6 +85,13 @@ instance PathPiece SheetFileType where
fromPathPiece t =
lookup (CI.mk t) [(CI.mk $ toPathPiece ty,ty) | ty <- [minBound..maxBound]]
-- $(deriveSimpleWith ''DisplayAble 'display (drop 17) ''SheetFileType)
instance DisplayAble SheetFileType where -- deprecated, see RenderMessage instance in Foundation
display SheetExercise = "Aufgabenstellung"
display SheetHint = "Hinweise"
display SheetSolution = "Musterlösung"
display SheetMarking = "Korrekturhinweise"
data ExamStatus = Attended | NoShow | Voided
deriving (Show, Read, Eq, Ord, Enum, Bounded)
derivePersistField "ExamStatus"

View File

@ -99,8 +99,6 @@ instance DisplayAble Text where
instance DisplayAble String where
display = pack
instance DisplayAble a => DisplayAble (Maybe a) where
display Nothing = ""
display (Just x) = display x
@ -130,6 +128,12 @@ trd3 (_,_,z) = z
-- snd3 = $(projNI 3 2)
-----------
-- Lists --
-----------
-- notNull = not . null
----------
-- Maps --
@ -153,6 +157,16 @@ mcons :: Maybe a -> [a] -> [a]
mcons Nothing xs = xs
mcons (Just x) xs = x:xs
newtype NTop a = NTop a -- treat Nothing as Top for Ord (Maybe a); default implementation treats Nothing as bottom
instance Eq a => Eq (NTop (Maybe a)) where
(NTop x) == (NTop y) = x == y
instance Ord a => Ord (NTop (Maybe a)) where
compare (NTop Nothing) (NTop Nothing) = EQ
compare (NTop Nothing) _ = GT
compare _ (NTop Nothing) = LT
compare (NTop (Just x)) (NTop (Just y)) = compare x y
---------------
-- Exception --

View File

@ -55,6 +55,9 @@ altFun perm = lamE pat rhs
deriveShowWith :: (String -> String) -> Name -> Q [Dec]
deriveShowWith = deriveSimpleWith ''Show 'show
-- deriveDisplayWith :: (String -> String) -> Name -> Q [Dec]
-- deriveDisplayWith = deriveSimpleWith ''DisplayAble 'display
deriveSimpleWith :: Name -> Name -> (String -> String) -> Name -> Q [Dec]
deriveSimpleWith cls fun strOp ty = do
(TyConI tyCon) <- reify ty

5
static/css/fontawesome.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,29 @@
@font-face {
font-family: 'Glyphicons Halflings';
src: url('../fonts/glyphicons-halflings-regular.eot');
src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),
url('../fonts/glyphicons-halflings-regular.woff') format('woff'),
url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),
url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
src: url('../fonts/glyphicons/glyphicons-halflings-regular.eot');
src: url('../fonts/glyphicons/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('../fonts/glyphicons/glyphicons-halflings-regular.woff2') format('woff2'),
url('../fonts/glyphicons/glyphicons-halflings-regular.woff') format('woff'),
url('../fonts/glyphicons/glyphicons-halflings-regular.ttf') format('truetype'),
url('../fonts/glyphicons/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
}
/*!
* Font Awesome Free 5.1.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
@font-face{
font-family:"Font Awesome 5 Free";
font-style:normal;
font-weight:900;
src:url(../fonts/fontawesome/fa-solid-900.eot);
src:url(../fonts/fontawesome/fa-solid-900.eot?#iefix) format("embedded-opentype"),
url(../fonts/fontawesome/fa-solid-900.woff2) format("woff2"),
url(../fonts/fontawesome/fa-solid-900.woff) format("woff"),
url(../fonts/fontawesome/fa-solid-900.ttf) format("truetype"),
url(../fonts/fontawesome/fa-solid-900.svg#fontawesome) format("svg");
}
.fa,.fas{
font-family:"Font Awesome 5 Free";
font-weight:900;
}

View File

@ -26,6 +26,13 @@
.glyphicon--user::before {
content: '\e008';
}
.glyphicon--group::before {
/* TODO: get updated glyphicons for group-icon */
content: '\e284';
}
.glyphicon--education::before {
content: '\e233';
}
.glyphicon--login::before {
content: '\e161';
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,44 @@
<div .container>
<h1>Uniworky - Admin Demopage
<p>
Diese interne Seite dient lediglich zum Testen diverser Funktionalitäten
und zur Demonstration der verschiedenen Hilfsfunktionen/Module.
Der Handler sollte jeweils aktuelle Beispiele für alle möglichen Funktionalitäten enthalten, so dass man immer weiß, wo man nachschlagen kann.
<div .container>
<h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
<ul>
<li .list-group-item>
<a href=@{UsersR}>Benutzer Verwaltung
<li .list-group-item>
<a href=@{TermShowR}>Semester Verwaltung
<a href=@{TermEditR}>Neues Semester anlegen
<li .list-group-item>
<a href=@{CourseNewR}>Kurse anlegen
<li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
<hr>
<div .container>
<h2>Funktionen zum Testen
<ul>
<li>
Knopf-Test:
<form .form-inline method=post action=@{AdminTestR} enctype=#{btnEnctype}>
^{btnWdgt}
<li><br>
Modals:
^{modal ".toggler1" Nothing}
<a href="/" .btn.toggler1>Klick mich für Ajax-Test
<noscript>(Für Modals bitte JS aktivieren)</noscript>
^{modal ".toggler2" (Just "Test Inhalt für Modal")}
<div .btn.toggler2>Klick mich für Content-Test
<noscript>(Für Modals bitte JS aktivieren)</noscript>

View File

@ -14,7 +14,7 @@
<tr>
<th #website>Website
<td>
<a href=#{link}>#{link}
<a href=#{link} target="_blank" rel="noopener" title="Website des Kurses">#{link}
<tr>
<th #participants>Teilnehmer
<td>
@ -35,7 +35,7 @@
$# if allowed to register
<div .course__registration.container>
<button class="btn btn-primary">
<a href="#">Anmelden
<a href="#">TODO: Kurs-Anmeldung
$# <form method=post action=@{CourseR tid csh CShow} enctype=#{regEnctype}>
$# ^{regWidget}
@ -45,52 +45,10 @@
<div .tab data-tab-name="Übungsblätter">
^{modal "#modal-toggler__new-sheet" Nothing}
<h3 .tab-title>Übungsblätter
<table .table.table-striped.table-hover>
<thead>
<tr>
<th>Blatt
<th>Abgabe ab
<th>Abgabe bis
<th>Bewertung</th>
<tbody>
<tr>
<td>
<a href="http://localhost:3000/course/S2018/ffp/ex/Blatt%201/show" role="button">Blatt 1
<td>Do 08.04.18
<td>Do 11.04.18
<td>NotGraded
<tr>
<td>
<a href="http://localhost:3000/course/S2018/ffp/ex/Blatt%201/show" role="button">Blatt 2
<td>Do 15.04.18
<td>Do 18.04.18
<td>NotGraded
<h1>TODO: Sortierbare Tabelle der bisherigen Übungsblätter
<div .tab data-tab-name="Übungsgruppen">
<h3 .tab-title>Übungsgruppen
<table .table.table-striped.table-hover>
<thead>
<tr>
<th>Name
<th>Termin
<th>Raum
<th>Studenten
<th>Tutor
<th>Anmeldung bis
<tbody>
<tr>
<td>Gruppe 1
<td>Montag 10:00 - 12:00
<td>N/A
<td>2/10
<td>Tutor1 Tutoren
<td>Do 21.02.2019, 19:00
<tr>
<td>Gruppe 2
<td>Montag 12:00 - 14:00
<td>N/A
<td>0/10
<td>Assistant1 Assistant
<td>Di 21.02.2017, 19:00
<h1>TODO: Sortierbare Tabelle der Übungsgruppen
<div .tab data-tab-name="Klausuren">
<h3 .tab-title>Klausuren
<div>...

View File

@ -14,9 +14,11 @@
<div .main__content-body>
$maybe headline <- contentHeadline
<h1>
<h1>
$maybe headline <- contentHeadline
^{headline}
$nothing
HEADLINE MISSING!
<!-- prime page actions -->
^{pageactionprime}

View File

@ -129,6 +129,21 @@ h4 {
margin: 0;
}
@media (max-width: 768px) {
h1 {
font-size: 24px;
}
h2 {
font-size: 20px;
}
h3 {
font-size: 16px;
}
}
/* LAYOUT */
.main {
min-height: calc(100vh - var(--header-height));
@ -157,7 +172,7 @@ h4 {
}
.main__content-body {
padding: 10px 40px 60px;
padding: 0 40px 60px;
}
@media (max-width: 768px) {
@ -239,3 +254,101 @@ a.btn.btn-info:hover,
.btn.btn-info:hover {
background-color: var(--color-grey)
}
/* GENERAL TABLE STYLES */
.table {
margin: 21px 0;
width: 100%;
}
.table--striped {
.table__row:not(.no-stripe):nth-child(even) {
background-color: rgba(0, 0, 0, 0.03);
}
}
.table--hover {
.table__row:not(.no-hover):not(.table__row--head):hover {
background-color: rgba(0, 0, 0, 0.07);
}
}
/* SCROLLTABLE */
.scrolltable {
overflow: auto;
}
/* TABLE DESIGN */
.table__row {
/* TODO: move outside of table__row as soon as tds and ths get their own class */
/* .table__td, .table__th { */
td, th {
padding-top: 14px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
max-width: 300px;
}
/* .table__td { */
td {
font-size: 16px;
color: #808080;
line-height: 1.4;
vertical-align: top;
}
/* .table__th { */
th {
background-color: var(--color-dark);
position: relative;
font-size: 16px;
color: #fff;
line-height: 1.4;
padding-top: 10px;
padding-bottom: 10px;
font-weight: bold;
text-align: left;
}
}
@media (max-width: 1200px) {
.table th {
padding: 4px 6px;
}
}
.table__td-content {
max-height: 100px;
overflow-y: auto;
}
.table__th-link {
color: white;
font-weight: bold;
&:hover {
color: inherit;
}
}
.table--vertical {
th {
background-color: transparent;
color: var(--color-font);
width: 170px;
text-align: right;
padding-right: 15px;
font-weight: 400;
}
td {
font-weight: 600;
color: var(--color-font);
}
}

View File

@ -0,0 +1,15 @@
<div .notification .notification-danger>
<div .notification__content>
<h1>
Hinweis zum Datenschutz
<p>
Dieses experimentelle Programm wurde noch nicht
hinsichtlich des Datenschutzes überprüft.
<em>
Die Benutzung erfolgt derzeit freiwillig und auf eigene Gefahr!
Wir sind natürlich bemüht, alle Datenschutzrechtlichen Vorgaben
zu erfüllen, doch eine Überprüfung kann erst stattfinden,
sobald die Software weitestgehend fertiggestellt wurde und
sich nicht mehr verändert. Um dies zu Erreichen sind jedoch Test
unter realen Bedingungen erforderlich. Wir bitten um Ihr Verständnis.

View File

@ -0,0 +1,50 @@
.notification {
position: relative;
border-radius: 3px;
padding: 10px 20px 20px;
margin: 40px 0;
color: var(--color-lighter);
box-shadow: 0 0 4px 2px inset currentColor;
padding-left: 20%;
color: #318dc5 ;
&::before {
content: 'i';
position: absolute;
display: flex;
left: 0;
top: 0;
height: 100%;
width: 20%;
font-size: 100px;
align-items: center;
justify-content: center;
}
}
@media (max-width: 768px) {
.notification {
padding-left: 40px;
&::before {
height: auto;
width: 45px;
font-size: 40px;
top: 15px;
}
}
}
.notification-danger {
color: #c51919 ;
&::before {
content: '!';
}
}
.notification__content {
color: var(--color-font);
}

View File

@ -1,46 +1,14 @@
<div .container>
<h1>Uniworky - Demo
<h3>
Testumgebung für die Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
<p>
Die Reimplementierung von
UniWorX ist noch nicht abgeschlossen.
Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
<div .alert .alert-danger>
<div .alert__content>Das System ist noch nicht produktiv einsetzbar
<div .alert__content>
Vorabversion!
Die Implementierung von
UniWorkY ist noch nicht abgeschlossen.
<hr>
<div .container>
<h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
<h1>Kurse mit offener Registrierung
<div .container>
^{courseTable}
<ul>
<li .list-group-item>
<a href=@{UsersR}>Benutzer Verwaltung
<li .list-group-item>
<a href=@{TermShowR}>Semester Verwaltung
<a href=@{TermEditR}>Neues Semester anlegen
<li .list-group-item>
<a href=@{CourseNewR}>Kurse anlegen
<li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
<hr>
<div .container>
<h2>Funktionen zum Testen
<ul>
<li>
Knopf-Test:
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
^{btnWdgt}
<li><br>
Modals:
^{modal ".toggler1" Nothing}
<a href="/" .btn.toggler1>Klick mich für Ajax-Test
<noscript>(Für Modals bitte JS aktivieren)</noscript>
^{modal ".toggler2" (Just "Test Inhalt für Modal")}
<div .btn.toggler2>Klick mich für Content-Test
<noscript>(Für Modals bitte JS aktivieren)</noscript>

23
templates/homeUser.hamlet Normal file
View File

@ -0,0 +1,23 @@
<div .container>
<h3>
Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
<div .alert .alert-danger>
<div .alert__content>
Vorabversion!
Die Implementierung von
UniWorkY ist noch nicht abgeschlossen.
<h1>Anstehende Übungsblätter
<div .container>
^{sheetTable}
<h1>
Anstehende Klausuren
TODO
<h1>
Anstehende Kursanmeldungen
TODO

View File

@ -7,8 +7,14 @@ $forall FileUploadInfo{..} <- fileInfos
<label for=#{fuiHtmlId}>
$# new files
<input type="file" name=#{fieldName} multiple>
<input type="file" name=#{fieldName} id=#{fieldId} multiple :req:required="required">
<div .file-input__multi-info>
_{MsgMultiFileUploadInfo}
<div .file-input__unpack>
<label for=#{fieldId}_zip>ZIPs entpacken
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips} :req:required>
<label for=#{fieldId}_zip>ZIPs automatisch entpacken
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips}>
<div class="js-tooltip">
<div class="tooltip__handle">?
<div class="tooltip__content hidden">Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu.

View File

@ -0,0 +1,24 @@
.file-input__unpack {
font-size: .9rem;
display: flex;
align-items: center;
margin-top: 10px;
.checkbox {
display: inline-block;
margin-left: 5px;
}
}
.file-input__multi-info {
font-size: .9rem;
font-style: italic;
margin-top: 10px;
color: var(--color-fontsec);
}
.file-input__list {
margin-left: 15px;
margin-top: 10px;
font-weight: 600;
}

View File

@ -1,80 +1,77 @@
<div .ui.container>
<div .profile>
<h1>
Access granted!
<div .scrolltable>
<table .table.table--striped.table--hover.table--vertical>
<tr.table__row>
<th> _{MsgName}
<td> #{display userDisplayName}
<tr.table__row>
<th> _{MsgMatrikelNr}
<td> #{display userMatrikelnummer}
<tr.table__row>
<th> _{MsgEMail}
<td> #{display userEmail}
<tr.table__row>
<th> _{MsgIdent}
<td> #{display userIdent}
<tr.table__row>
<th> _{MsgPlugin}
<td> #{display userPlugin}
$if not $ null admin_rights
<tr.table__row>
<th> Administrator
<td>
<ul>
$forall institute <- admin_rights
<li>#{display institute}
$if not $ null lecturer_rights
<tr.table__row>
<th> Lehrberechtigt
<td>
<ul>
$forall institute <- lecturer_rights
<li>#{display institute}
$if not $ null lecture_owner
<tr.table__row>
<th> Eigene Kurse
<td>
<ul>
$forall (E.Value csh, E.Value tid) <- lecture_owner
<li>
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
$if not $ null lecture_corrector
<tr.table__row>
<th> Korrektor
<td>
<ul>
$forall (E.Value csh, E.Value tid) <- lecture_corrector
<li>
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
$if not $ null studies
<tr.table__row>
<th> Studiengänge
<td>
<table .table .table-striped .table-hover>
<tr.table__row>
<th> Abschluss
<th> Studiengang
<th> Studienart
<th> Semester
<p>
This page is protected and access is allowed only for authenticated users.
<p>
Your data is protected with us <strong><span class="username">#{userIdent}</span></strong>!
$if not $ null admin_rights
<h1>
Administrator für die Institute
<ul>
$forall institute <- admin_rights
<li>#{display institute}
$if not $ null lecturer_rights
<h1>
Lehrberechtigung für die Institute
<ul>
$forall institute <- lecturer_rights
<li>#{display institute}
<h2>
Zugriffsberechtigung als Lehrender auf:
<ul>
$forall (E.Value csh, E.Value tid) <- lecture_owner
<li>
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
<h2>
Zugriffsberechtigung als Korrekor auf:
<ul>
$forall (E.Value csh, E.Value tid) <- lecture_corrector
<li>
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
<h2>
Kursteilnehmer:
<ul>
$forall (E.Value csh, E.Value tid, regSince) <- participant
<li>
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
registriert seit #{display regSince}
<h2>
Abgegebene Übungsblätter:
TODO
<p>
<h1>
Benutzerdaten
^{userTable}
<h2>
Studiengänge
<ul>
$forall (degree,field,fieldtype,semester) <- studies
<li>#{display degree}
#{display field}
#{display fieldtype}
#{display semester}
<em> TODO: Mehr Daten in Tabelle anzeigen!
<h2>
Alle Benutzerbezogenen Daten (Abgaben, Klausurnoten, etc.)
<p>
<em> TODO: Alle Abgaben, Klausurnoten finden und verlinken
<h2>
<em> TODO: Knopf zum Löschen der Daten erstellen
<p>
<h4>Hinweise:
<ul>
<li>
Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrekturen von Übungen, Übungsgruppenleiterschaft, Raumbuchungen, etc.
<li>
Benutzerdaten bleiben so lange gespeichert, bis ein Institutsadministrator über die Exmatrikulation informiert wurde. Dann wird der Account gelöscht.
Abgaben/Bonuspunkte werden unwiderruflich gelöscht.
Klausurnoten verbleiben aus statistischen Gründen anonymisiert im System.
<li>
Bei gemeinsamen Gruppenabgaben wird nur die Zuordnung zu diesem Benutzer gelöscht.
Die Abgabe selbst wird erst gelöscht, wenn alle Benutzer einer Abgabe deren Löschung veranlasst haben.
$forall (degree,field,fieldtype,semester) <- studies
<tr.table__row>
<td> #{display degree}
<td> #{display field}
<td> #{display fieldtype}
<td> #{display semester}
$if not $ null participant
<tr.table__row>
<th> Teilnehmer
<td>
<ul>
$forall (E.Value csh, E.Value tid, regSince) <- participant
<li>
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
seit #{display regSince}
^{settingsForm}

View File

@ -0,0 +1,24 @@
<div .container>
<div .alert .alert-danger>
<div .alert__content>
TODO: Alle Benutzerbezogenen Daten sollen hier angezeigt
und verlinkt werden
(alle Abgaben, Klausurnoten, etc.)
<em> TODO: Hier mehr Daten in Tabellen anzeigen!
<h2>
<em> TODO: Knopf zum Löschen aller Daten erstellen
<p>
<h4>Hinweise:
<ul>
<li>
Nicht aufgeführt sind Zeitstempel mit Benutzerinformationen, z.B. bei der Editierung und Korrekturen von Übungen, Übungsgruppenleiterschaft, Raumbuchungen, etc.
<li>
Benutzerdaten bleiben so lange gespeichert, bis ein Institutsadministrator über die Exmatrikulation informiert wurde. Dann wird der Account gelöscht.
Abgaben/Bonuspunkte werden unwiderruflich gelöscht.
Klausurnoten verbleiben aus statistischen Gründen anonymisiert im System.
<li>
Bei gemeinsamen Gruppenabgaben wird nur die Zuordnung zu diesem Benutzer gelöscht.
Die Abgabe selbst wird erst gelöscht, wenn alle Benutzer einer Abgabe deren Löschung veranlasst haben.

View File

@ -13,7 +13,7 @@
<h2 #description>Hinweise
<p> #{descr}
<h3>Bewertung
<p> #{show $ sheetType sheet}
<p> #{display $ sheetType sheet}
$maybe marking <- sheetMarkingText sheet
<p> #{marking}
<br>

View File

@ -4,133 +4,67 @@
window.utils = window.utils || {};
// allows for multiple file uploads with separate inputs
window.utils.reactiveFileUpload = function(input, formGroup) {
var currValidInputCount = 0;
var addMore = false;
var inputName = input.getAttribute('name');
var isMulti = input.hasAttribute('multiple') ? true : false;
var wrapper = formGroup;
// FileInput PseudoClass
function FileInput(container, input, label, remover) {
this.container = container;
this.input = input;
this.label = label;
this.remover = remover;
addListener(this);
window.utils.initializeFileUpload = function(input) {
var isMulti = input.hasAttribute('multiple');
var fileList = isMulti ? addFileList() : null;
var label = addFileLabel();
this.addTo = function(parentElement) {
parentElement.appendChild(this.container);
}
this.remove = function() {
this.container.remove();
}
this.wasValid = function() {
return this.container.classList.contains('file-input__container--valid');
}
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 addNextInput() {
var inputs = wrapper.querySelectorAll('.file-input__container');
if (inputs[inputs.length - 1].classList.contains('file-input__container--valid')) {
makeInput(inputName).addTo(wrapper);
}
}
// updates submitbutton and form-group-stripe
function updateForm() {
var submitBtn = formGroup.parentElement.querySelector('[type=submit]');
formGroup.classList.remove('form-group--has-error');
if (currValidInputCount > 0) {
if (formGroup.classList.contains('form-group')) {
formGroup.classList.add('form-group--valid')
}
function updateLabel(files) {
if (files.length) {
if (isMulti) {
addNextInput();
label.innerText = files.length + ' Dateien ausgwählt';
} else {
label.innerHTML = files[0].name;
}
} else {
if (formGroup.classList.contains('form-group')) {
formGroup.classList.remove('form-group--valid')
}
resetFileLabel();
}
}
// addseventlistener destInput
function addListener(fileInput) {
fileInput.input.addEventListener('change', function(event) {
if (fileInput.input.value.length > 0) {
// update label
var filePath = fileInput.input.value.replace(/\\/g, '/').split('/');
var fileName = filePath[filePath.length - 1];
fileInput.label.innerHTML = fileName;
// increase count if this field was empty previously
if (!fileInput.wasValid()) {
currValidInputCount++;
}
fileInput.container.classList.add('file-input__container--valid')
// show next input
} else {
if (isMulti) {
currValidInputCount--;
}
clearInput(fileInput);
}
updateForm();
});
fileInput.input.addEventListener('focus', function() {
fileInput.container.classList.add('pseudo-focus');
});
fileInput.input.addEventListener('blur', function() {
fileInput.container.classList.remove('pseudo-focus');
});
fileInput.remover.addEventListener('click', function() {
if (fileInput.wasValid()) {
currValidInputCount--;
}
clearInput(fileInput);
});
}
// clears or removes fileinput based on multi-file or not
function clearInput(fileInput) {
if (isMulti) {
fileInput.remove();
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 {
fileInput.container.classList.remove('file-input__container--valid')
fileInput.label.innerHTML = '';
input.parentElement.appendChild(list);
}
updateForm();
return list;
}
// create new wrapped input element with name name
function makeInput(name) {
var cont = document.createElement('div');
var desc = document.createElement('label');
var nextInput = document.createElement('input');
var remover = document.createElement('div');
cont.classList.add('file-input__container');
desc.classList.add('file-input__label', 'btn');
nextInput.classList.add('js-file-input');
desc.setAttribute('for', name + '-' + currValidInputCount);
remover.classList.add('file-input__remover');
nextInput.setAttribute('id', name + '-' + currValidInputCount);
nextInput.setAttribute('name', name);
nextInput.setAttribute('type', 'file');
cont.appendChild(nextInput);
cont.appendChild(desc);
cont.appendChild(remover);
return new FileInput(cont, nextInput, desc, remover);
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
function setup() {
var newInput = makeInput(inputName);
resetFileLabel();
input.classList.add('file-input__input--hidden');
input.addEventListener('change', function() {
if (isMulti) {
wrapper = document.createElement('div');
wrapper.classList.add('file-input__wrapper');
console.log(wrapper);
// TODO: fix file input
formGroup.insertBefore(wrapper, input);
renderFileList(input.files);
}
input.remove();
newInput.addTo(wrapper);
updateForm();
}
setup();
updateLabel(input.files);
});
}
// to remove previously uploaded files
@ -167,6 +101,7 @@
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);
@ -174,7 +109,11 @@
wrapperEl.appendChild(input);
wrapperEl.appendChild(labelEl);
parentEl.appendChild(wrapperEl);
if (siblingEl) {
parentEl.insertBefore(wrapperEl, siblingEl);
} else {
parentEl.appendChild(wrapperEl);
}
}
}
@ -194,11 +133,7 @@ document.addEventListener('DOMContentLoaded', function() {
// initialize file-upload-fields
Array.from(document.querySelectorAll('input[type="file"]')).forEach(function(inp) {
var formGroup = inp.parentNode;
while (!formGroup.classList.contains('form-group') && formGroup !== document.body) {
formGroup = formGroup.parentNode;
}
window.utils.reactiveFileUpload(inp, formGroup);
window.utils.initializeFileUpload(inp);
});
// initialize file-checkbox-fields

View File

@ -11,16 +11,34 @@ form {
grid-template-columns: 1fr 3fr;
grid-gap: 5px;
justify-content: flex-start;
align-items: baseline;
align-items: flex-start;
padding: 4px;
border-left: 2px solid transparent;
+ .form-group {
margin-top: 17px;
margin-top: 13px;
}
}
.form-group__label {
font-weight: 600;
padding-top: 6px;
}
.form-group--required {
.form-group__label::after {
content: ' *';
color: var(--color-error);
}
}
.form-group--has-error {
background-color: rgba(255, 0, 0, 0.1);
input, textarea {
border-color: var(--color-error) !important;
}
}
@media (max-width: 768px) {
@ -63,33 +81,6 @@ input[type*="time"] {
min-width: 240px;
}
.form-group--required {
.form-group__label::before {
content: '*';
position: absolute;
left: -14px;
}
input, textarea {
border-bottom-color: var(--color-primary);
}
}
.form-group--valid {
input, textarea {
border-bottom-color: var(--color-success);
}
}
.form-group--has-error {
input, textarea {
border-bottom-color: var(--color-error);
}
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="url"]:focus,
@ -120,6 +111,7 @@ textarea {
border: 1px solid #dbdbdb;
border-radius: 2px;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
vertical-align: top;
}
textarea:focus {
@ -128,6 +120,20 @@ textarea:focus {
outline: 0;
}
/* OPTIONS */
select,
option {
font-size: 1rem;
line-height: 1.5;
padding: 4px 13px;
border: 1px solid #dbdbdb;
border-radius: 2px;
outline: 0;
color: #363636;
background-color: #f3f3f3;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
}
/* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */
input[type="checkbox"] {
position: relative;
@ -176,9 +182,10 @@ input[type="checkbox"]:checked::after {
label {
display: block;
height: 30px;
width: 30px;
background-color: var(--color-grey);
height: 24px;
width: 24px;
background-color: #f3f3f3;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
border-radius: 4px;
color: white;
cursor: pointer;
@ -188,25 +195,14 @@ input[type="checkbox"]:checked::after {
label::after {
content: '';
position: absolute;
top: 14px;
left: 5px;
top: 11px;
left: 3px;
display: block;
width: 20px;
height: 20px;
background-color: white;
width: 18px;
height: 2px;
background-color: var(--color-font);
transition: all .2s;
}
label::before {
width: 20px;
height: 2px;
transform: scale(0.1, 0.1);
}
label::after {
width: 20px;
height: 2px;
transform: scale(0.1, 0.1);
transform: scale(0.5, 0.1);
}
:checked + label {
@ -215,10 +211,12 @@ input[type="checkbox"]:checked::after {
}
:checked + label::before {
background-color: white;
transform: scale(1, 1) rotate(45deg);
}
:checked + label::after {
background-color: white;
transform: scale(1, 1) rotate(-45deg);
}
}
@ -243,114 +241,17 @@ input[type="checkbox"]:checked::after {
}
/* CUSTOM FILE INPUT */
input[type="file"].js-file-input {
color: white;
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
outline: 0;
border: 0;
}
.file-input__wrapper {
grid-column-start: 2;
}
.file-input__container,
.file-checkbox__container,
.file-input__unpack {
grid-column-start: 2;
margin: 4px 0;
}
.file-input__label,
.file-input__remover,
.file-checkbox__label,
.file-checkbox__remover {
display: block;
border-radius: 2px;
padding: 5px 13px;
color: var(--color-lightwhite);
.file-input__label {
cursor: pointer;
display: inline-block;
background-color: var(--color-primary);
color: white;
padding: 10px 17px;
border-radius: 3px;
}
.file-input__label,
.file-checkbox__label {
text-align: left;
position: relative;
height: 30px;
}
.file-checkbox__label {
background-color: var(--color-grey);
text-decoration: line-through;
}
.file-input__label.btn,
.file-checkbox__label.btn {
padding: 5px 13px;
}
.file-input__label::after,
.file-input__label::before {
position: absolute;
content: '';
background-color: white;
width: 16px;
height: 2px;
top: 14px;
top: 50%;
left: 12px;
left: 50%;
}
.file-input__list {
.file-input__label::after {
transform: translate(-50%, -50%) rotate(90deg);
}
.file-input__label::before {
transform: translate(-50%, -50%);
}
.file-checkbox__checkbox {
margin-left: 10px;
}
.file-input__remover {
.file-input__input--hidden {
display: none;
width: 40px;
height: 30px;
text-align: center;
background-color: var(--color-warning);
position: relative;
margin-left: 10px;
}
.file-input__remover::before {
position: absolute;
content: '';
width: 16px;
height: 2px;
top: 14px;
left: 12px;
background-color: white;
}
.file-input__container--valid > .file-input__label {
background-color: var(--color-light);
}
.file-checkbox__container--checked > .file-checkbox__label {
text-decoration: none;
background-color: var(--color-lighter);
&.btn:hover {
background-color: var(--color-lighter);
text-decoration: line-through;
}
}
.file-input__container--valid > .file-input__label::before,
.file-input__container--valid > .file-input__label::after {
content: none;
}
.file-input__container--valid > .file-input__remover {
display: block;
}
@media (max-width: 768px) {
.file-input__wrapper,
.file-input__container,
.file-checkbox__container,
.file-input__unpack {
grid-column-start: 1;
}
}

View File

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

View File

@ -0,0 +1,37 @@
(function() {;
'use strict';
window.utils = window.utils || {};
// allows for multiple file uploads with separate inputs
window.utils.tooltip = function(tt) {
var handle = tt.querySelector('.tooltip__handle');
var content = tt.querySelector('.tooltip__content');
var left = false;
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);
});
}
})();
document.addEventListener('DOMContentLoaded', function() {
// initialize tooltips
Array.from(document.querySelectorAll('.js-tooltip')).forEach(function(tt) {
window.utils.tooltip(tt);
});
});

View File

@ -0,0 +1,58 @@
.js-tooltip {
position: relative;
.tooltip__handle {
background-color: var(--color-dark);
border-radius: 50%;
height: 1.5rem;
width: 1.5rem;
line-height: 1.5rem;
font-size: 1.2rem;
color: white;
display: inline-block;
text-align: center;
cursor: default;
margin: 0 10px;
}
.tooltip__content {
position: absolute;
top: -10px;
transform: translateY(-100%);
left: 3px;
width: 275px;
z-index: 10;
background-color: #fafafa;
border-radius: 4px;
padding: 13px 17px;
box-shadow: 0 0 20px 4px rgba(0, 0, 0, 0.1);
// display: none;
&.to-left {
left: auto;
right: 3px;
&::after {
left: auto;
right: 10px;
}
}
&::after {
content: '';
width: 16px;
height: 16px;
background-color: #fafafa;
transform: rotate(45deg);
position: absolute;
left: 10px;
// box-shadow: 0 0 4px black;
// outline: 1px solid red;
bottom: -8px;
}
}
.hidden {
display: none;
}
}

View File

@ -46,7 +46,7 @@
#{fileTitle file}&nbsp;
<span .label .label-warning>Gelöscht
$else
<a href=@{SubmissionDownloadSingleR cID $ fileTitle file} download .list-group-item>
<a href=@{SubmissionDownloadSingleR cID' $ fileTitle file} download .list-group-item>
#{fileTitle file}
$if submissionFileIsUpdate sFile
&nbsp;

View File

@ -1,76 +1,4 @@
.table {
margin: 21px 0;
width: 100%;
}
.table--striped {
.table__row:not(.no-stripe):nth-child(even) {
background-color: rgba(0, 0, 0, 0.03);
}
}
.table--hover {
.table__row:not(.no-hover):not(.table__row--head):hover {
background-color: rgba(0, 0, 0, 0.07);
}
}
/* TABLE DESIGN */
.table__row {
/* TODO: move outside of table__row as soon as tds and ths get their own class */
/* .table__td, .table__th { */
td, th {
padding-top: 14px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
max-width: 300px;
}
/* .table__td { */
td {
font-size: 16px;
color: #808080;
line-height: 1.4;
vertical-align: top;
}
&.table__row--head {
background-color: var(--color-dark);
}
/* .table__th { */
th {
position: relative;
font-size: 16px;
color: #fff;
line-height: 1.4;
padding-top: 20px;
padding-bottom: 15px;
font-weight: bold;
text-align: left;
}
}
.table__td-content {
max-height: 100px;
overflow-y: auto;
}
.table__th-link {
color: white;
font-weight: bold;
&:hover {
color: inherit;
}
}
/* SORTABLE */
/* SORTABLE TABLE */
.table {
/* TODO: move outside of table as soon as tds and ths get their own class */

View File

@ -37,8 +37,3 @@
}
}
}
/* SCROLLTABLE */
.scrolltable {
overflow: auto;
}

View File

@ -1,23 +1,12 @@
$newline never
<aside .main__aside>
<div .asidenav>
<div .asidenav__box>
<ul .asidenav__list>
$forall menuType <- menuTypes
$case menuType
$of NavbarAside (MenuItem label mIcon route _)
<li .asidenav__list-item :highlight route:.asidenav__list-item--active>
<a .asidenav__link-wrapper href=@{route}>
<div .glyphicon.glyphicon--#{fromMaybe "none" mIcon}>
<div .asidenav__link-label>#{label}
$of _
<div .asidenav__box>
<h3 .asidenav__box-title>
$# TODO: this has to come from favourites somehow. Show favourites from older terms?
WiSe 17/18
<ul .asidenav__list>
$forall (Course{..}, courseRoute, pageActions) <- favourites
$# TODO: this list-item should also be marked as active if one of its children is shown
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
<a .asidenav__link-wrapper href=@{courseRoute}>
<div .asidenav__link-shorthand>#{courseShorthand}

View File

@ -6,16 +6,26 @@
flex: 0 0 300px;
min-height: calc(100% - 80px);
transition: all .2s ease-out;
width: 300px;
width: 24%;
~ .main__content {
padding-left: 300px;
padding-left: 24%;
transition: padding-left .2s ease-out;
}
}
/* maximum width of 300px for wide screens */
@media (min-width: 1200px) {
.main__aside {
width: 300px;
~ .main__content {
padding-left: 300px;
}
}
}
.asidenav {
width: 300px;
color: white;
}
@ -33,7 +43,6 @@
.asidenav__list-item {
position: relative;
color: var(--color-lightwhite);
padding-left: 10px;
&:hover {
color: var(--color-link);
@ -61,6 +70,14 @@
}
}
/* small list-item-padding for medium to large screens */
@media (min-width: 1024px) {
.asidenav__list-item {
padding-left: 10px;
}
}
.asidenav__list-item--active {
background-color: var(--color-lightwhite);
@ -79,7 +96,7 @@
position: relative;
display: flex;
align-items: center;
padding: 7px 0;
padding: 7px 10px;
justify-content: flex-start;
color: var(--color-lightwhite);
z-index: 1;
@ -100,7 +117,6 @@
.asidenav__link-label {
line-height: 1;
padding-left: 13px;
}
/* hover sub-menus */
@ -153,11 +169,6 @@
padding-left: 50px;
}
&.pseudo-hover {
overflow: visible;
flex-basis: 300px;
}
.asidenav__box-title {
width: 50px;
padding: 1px;
@ -169,9 +180,6 @@
.asidenav__link-shorthand {
display: flex;
position: static;
// TODO: make shorthands in collapsed beautiful *.*
// background-color: var(--color-dark);
// color: var(--color-lightwhite);
height: 50px;
width: 50px;
text-align: center;
@ -196,22 +204,14 @@
}
}
.asidenav__list-item:hover {
> .asidenav__link-wrapper {
color: var(--color-dark);
background-color: var(--color-lightwhite);
}
}
.asidenav__link-wrapper {
color: var(--color-lightwhite);
padding: 0;
// background-color: var(--color-dark);
}
.asidenav__nested-list,
.asidenav__link-label {
padding-left: 0;
display: none;
}
.asidenav__list-item--active {
@ -221,5 +221,6 @@
color: var(--color-dark);
}
}
}
}

View File

@ -4,4 +4,4 @@ $newline never
$forall bc <- parents
<li .breadcrumbs__item>
<a .breadcrumbs__link href="@{fst bc}">#{snd bc}
<li ..breadcrumbs__item.breadcrumbs__last-item>#{title}
<li .breadcrumbs__item>#{title}

View File

@ -1,30 +1,42 @@
.breadcrumbs__container {
position: relative;
align-self: flex-end;
background-color: var(--color-dark);
color: white;
transition: margin-bottom .2s ease;
color: var(--color-font);
margin-left: 40px;
margin-top: 25px;
}
.breadcrumbs__container--animated {
transition: left .2s ease;
@media (max-width: 768px) {
.breadcrumbs__container {
margin-left: 20px;
}
}
.breadcrumbs__link {
color: var(--color-lightwhite);
&:hover {
color: var(--color-lightwhite);
color: var(--color-fontsec);
}
}
.breadcrumbs__item {
padding-left: 25px;
padding-right: 14px;
position: relative;
line-height: 28px;
opacity: 0.8;
z-index: 1;
margin-right: 10px;
&:last-child {
margin-right: 0;
font-weight: 800;
color: var(--color-dark);
top: 1px;
&::after {
content: none;
}
}
--color-separator: var(--color-primary);
@ -36,37 +48,13 @@
&::after {
content: '';
position: absolute;
top: 4px;
right: -13px;
width: 18px;
height: 18px;
border-bottom: 2px solid var(--color-separator);
border-right: 2px solid var(--color-separator);
top: 11px;
right: 0;
width: 7px;
height: 7px;
border-bottom: 1px solid var(--color-separator);
border-right: 1px solid var(--color-separator);
transform: rotate(-45deg);
z-index: 10;
}
}
.breadcrumbs__last-item {
padding-right: 20px;
background-color: var(--color-separator);
&::before {
content: '';
position: absolute;
top: 4px;
left: -8px;
width: 18px;
height: 18px;
background-color: var(--color-dark);
border-bottom: 2px solid var(--color-primary);
border-right: 2px solid var(--color-primary);
transform: rotate(-45deg);
z-index: 10;
}
&::after {
background-color: var(--color-separator);
right: -8px;
}
}

View File

@ -6,8 +6,6 @@ $case formLayout
<div .form-group :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error>
$if not (Blaze.null $ fvLabel view)
<label .form-group__label for=#{fvId view}>#{fvLabel view}
$# TODO: inputs should have proper placeholders
$# TODO: can we wrap checkboxes in a <div.checkbox> and radio buttons in a <div.radio>?
<div .form-group__input>
$# FIXME: file-input does not have `required` attribute, although set on form-group
^{fvInput view}

View File

@ -3,23 +3,25 @@
window.utils = window.utils || {};
// registers input-listener for each element in <elements> (array) and
// enables <button> if <validation> for these elements returns true
window.utils.reactiveButton = function(elements, button, validation) {
if (elements.length == 0) {
// 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) {
var requireds = Array.from(form.querySelectorAll('[required]'));
if (requireds.length == 0) {
return false;
}
var checkboxes = elements[0].getAttribute('type') === 'checkbox';
var eventType = checkboxes ? 'change' : 'input';
updateButtonState();
elements.forEach(function(el) {
requireds.forEach(function(el) {
var checkbox = el.getAttribute('type') === 'checkbox';
var eventType = checkbox ? 'change' : 'input';
el.addEventListener(eventType, function() {
updateButtonState();
});
});
function updateButtonState() {
if (validation.call(null, elements) === true) {
if (validation.call(null, requireds) === true) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', 'true');
@ -33,19 +35,21 @@ document.addEventListener('DOMContentLoaded', function() {
// auto reactiveButton submit-buttons with required fields
var forms = document.querySelectorAll('form');
Array.from(forms).forEach(function(form) {
var requireds = form.querySelectorAll('[required]');
var submitBtn = form.querySelector('[type=submit]');
if (submitBtn && requireds) {
window.utils.reactiveButton(Array.from(requireds), submitBtn, function validateForm(inputs) {
var done = true;
inputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
});
if (submitBtn) {
window.utils.reactiveButton(form, submitBtn, validateForm);
}
});
function validateForm(inputs) {
var done = true;
inputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
}
});

View File

@ -2,7 +2,17 @@ $newline never
<div .navbar-container>
<nav .navbar.js-sticky-navbar>
<div .navbar__logo>
<a href="/" .navbar__logo>
<ul .navbar__list.list--inline>
$forall menuType <- menuTypes
$case menuType
$of NavbarAside (MenuItem label mIcon route _)
<li .navbar__list-item :highlight route:.navbar__list-item--active>
<a .navbar__link-wrapper href=@{route}>
<i .fas.fa-#{fromMaybe "none" mIcon}>
<div .navbar__link-label>#{label}
$of _
<ul .navbar__list.list--inline>
$forall menuType <- menuTypes

View File

@ -8,32 +8,23 @@
init();
function init() {
window.setTimeout(function () {
nav.classList.add('navbar--animated');
}, 200);
checkScroll();
addListener();
}
// checks scroll direction and shows/hides navbar accordingly
function checkScroll() {
var sticky = window.scrollY > 0;
sticky = sticky && window.innerHeight < (document.scrollingElement.scrollHeight - 100);
nav.classList.toggle('navbar--sticky', sticky);
ticking = false;
}
function addListener() {
window.addEventListener('scroll', function (e) {
if (!ticking) {
window.requestAnimationFrame(checkScroll);
window.requestAnimationFrame(update);
ticking = true;
}
}, false);
update();
}
function update() {
var sticky = window.scrollY > 0;
sticky = sticky && window.innerHeight < (document.scrollingElement.scrollHeight - 200);
nav.classList.toggle('navbar--sticky', sticky);
ticking = false;
}
}
})();
document.addEventListener('DOMContentLoaded', function () {

View File

@ -6,8 +6,8 @@
justify-content: space-between;
width: 100%;
height: var(--header-height);
padding-right: 5vw;
padding-left: 20px;
padding-right: 2vw;
padding-left: calc(24% + 40px);
background: var(--color-darker); /* Old browsers */
background: -moz-linear-gradient(bottom, var(--color-dark) 0%, var(--color-darker) 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(bottom, var(--color-dark) 0%,var(--color-darker) 100%); /* Chrome10-25,Safari5.1-6 */
@ -17,28 +17,36 @@
top: 0;
left: 0;
overflow: hidden;
transition: height 0.2s cubic-bezier(0.03, 0.43, 0.58, 1);
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
transition: all .2s cubic-bezier(0.03, 0.43, 0.58, 1);
}
@media (min-width: 1200px) {
.navbar {
padding-left: 340px;
}
}
@media (max-width: 768px) {
.navbar {
padding-left: 90px;
}
}
@media (max-width: 500px) {
.navbar {
padding-left: 20px;
padding: 0px;
}
}
.navbar__logo {
position: absolute;
top: 15px;
left: 20px;
transition: all .2s ease;
transform-origin: left;
width: 0px;
color: var(--color-lightwhite);
&:hover {
color: var(--color-lightwhite);
}
&::before {
content: 'UniWorkY';
@ -49,43 +57,21 @@
}
}
@media (max-width: 768px) {
@media (max-width: 1024px) {
.navbar__logo {
transform: scale(0.7);
}
}
@media (max-width: 425px) {
@media (max-width: 768px) {
.navbar__logo {
transform: scale(0.3);
display: none;
}
}
.navbar__list {
align-self: flex-end;
white-space: nowrap;
}
.navbar__list-item {
position: relative;
transition: background-color .1s ease;
.glyphicon {
position: relative;
width: 100%;
height: 20px;
}
.glyphicon::before {
height: 20px;
}
}
.navbar :last-child {
margin-left: auto;
}
/* links */
.navbar__link-wrapper {
display: flex;
flex-direction: column;
@ -99,14 +85,67 @@
.navbar__link-label {
transition: opacity .2s ease;
padding: 0 13px;
color: var(--color-lightwhite);
text-transform: uppercase;
}
@media (max-width: 768px) {
.navbar__link-label {
padding: 0 7px;
}
}
@media (max-width: 500px) {
.navbar__link-label {
display: none;
}
}
/* navbar list */
.navbar__list {
white-space: nowrap;
}
/* list item */
.navbar__list-item {
position: relative;
transition: background-color .1s ease;
.glyphicon {
position: relative;
width: 20px;
height: 20px;
}
.glyphicon::before {
height: 20px;
}
.fas {
height: 20px;
}
}
@media (max-width: 500px) {
.navbar__list-item {
width: 50px;
}
}
.navbar__list-item--secondary {
margin-left: 20px;
color: var(--color-grey);
}
@media (max-width: 768px) {
.navbar__list-item--secondary {
margin-left: 0;
}
}
.navbar__list-item--secondary + .navbar__list-item--secondary {
margin-left: 0;
border-left: 0;
@ -120,27 +159,21 @@
color: var(--color-dark);
}
}
.navbar__list-item--active .navbar__link-wrapper {
pointer-events: none;
}
.navbar__list-item--active .navbar__link-label {
color: var(--color-dark);
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover {
background-color: var(--color-darker);
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-wrapper {
background-color: var(--color-darker);
color: var(--color-lightwhite);
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover .navbar__link-label {
color: var(--color-lightwhite);
}
.navbar__list-item--secondary .navbar__link-wrapper,
.navbar__list-item--secondary .navbar__link-label {
.navbar__list-item--secondary .navbar__link-wrapper {
color: var(--color-grey);
}
/* sticky state */
.navbar--sticky {
height: var(--header-height-collapsed);
z-index: 100;
@ -148,10 +181,10 @@
.navbar__link-wrapper {
height: var(--header-height-collapsed);
}
}
.navbar--animated {
transition: all .2s cubic-bezier(0.03, 0.43, 0.58, 1);
.navbar__logo {
top: 5px;
}
}
.navbar__pushdown {

View File

@ -1,5 +1,5 @@
.page-nav-prime {
margin-top: 13px;
margin: 13px 0;
}
.pagenav__list {