Merge branch 'master' of gitlab.cip.ifi.lmu.de:jost/UniWorX

This commit is contained in:
SJost 2018-07-01 13:53:36 +02:00
commit e5d62abbaf
43 changed files with 1510 additions and 562 deletions

View File

@ -22,6 +22,8 @@ InvalidInput: Eingaben bitte korrigieren.
Term: Semester Term: Semester
TermPlaceholder: W/S + vierstellige Jahreszahl TermPlaceholder: W/S + vierstellige Jahreszahl
LectureStart: Beginn Vorlesungen
Course: Kurs Course: Kurs
CourseSecret: Zugangspasswort CourseSecret: Zugangspasswort
CourseNewOk tid@TermId courseShortHand@Text: Kurs #{display tid}-#{courseShortHand} wurde erfolgreich erstellt. CourseNewOk tid@TermId courseShortHand@Text: Kurs #{display tid}-#{courseShortHand} wurde erfolgreich erstellt.
@ -70,6 +72,7 @@ DeprecatedRoute: Diese Ansicht ist obsolet und könnte in Zukunft entfallen.
UnfreeMaterials: Die Materialien für diese Veranstaltung sind nicht allgemein freigegeben. UnfreeMaterials: Die Materialien für diese Veranstaltung sind nicht allgemein freigegeben.
UnauthorizedWrite: Sie haben hierfür keine Schreibberechtigung UnauthorizedWrite: Sie haben hierfür keine Schreibberechtigung
Submission: Abgabenummer
SubmissionWrongSheet: Abgabenummer gehört nicht zum angegebenen Übungsblatt. SubmissionWrongSheet: Abgabenummer gehört nicht zum angegebenen Übungsblatt.
SubmissionAlreadyExists: Sie haben bereits eine Abgabe zu diesem Übungsblatt. SubmissionAlreadyExists: Sie haben bereits eine Abgabe zu diesem Übungsblatt.
@ -79,11 +82,27 @@ SubmissionArchive: Zip-Archiv der Abgabedatei(en)
SubmissionFile: Datei zur Abgabe SubmissionFile: Datei zur Abgabe
SubmissionAlreadyExistsFor user@Text: #{user} hat bereits eine Abgabe zu diesem bÜbungsblatt. SubmissionAlreadyExistsFor user@Text: #{user} hat bereits eine Abgabe zu diesem bÜbungsblatt.
CorrectionsTitle: Zugewiesene Korrekturen
CourseCorrectionsTitle: Korrekturen für diesen Kurs
EMail: E-Mail EMail: E-Mail
EMailUnknown email@Text: E-Mail #{email} gehört zu keinem bekannten Benutzer. EMailUnknown email@Text: E-Mail #{email} gehört zu keinem bekannten Benutzer.
NotAParticipant user@Text tid@TermId csh@Text: #{user} ist nicht im Kurs #{display tid}-#{csh} angemeldet. NotAParticipant user@Text tid@TermId csh@Text: #{user} ist nicht im Kurs #{display tid}-#{csh} angemeldet.
AddCorrector: Zusätzlicher Korrektor
CorrectorExists user@Text: #{user} ist bereits als Korrektor eingetragen
SheetCorrectorsTitle tid@TermIdentifier courseShortHand@Text sheetName@Text: Korrektoren für #{termToText tid}-#{courseShortHand} #{sheetName}
CountTutProp: Tutorien zählen gegen Proportion
Corrector: Korrektor
Correctors: Korrektoren
CorByTut: Nach Tutorium
CorProportion: Anteil
DeleteRow: Zeile entfernen
ProportionNegative: Anteile dürfen nicht negativ sein
CorrectorsUpdated: Korrektoren erfolgreich aktualisiert
CorrectorsPlaceholder: Korrektoren...
CorrectorsDefaulted: Korrektoren-Liste wurde aus bisherigen Übungsblättern diesen Kurses generiert. Es sind keine Daten gespeichert.
Users: Benutzer Users: Benutzer
HomeHeading: Aktuelle Termine HomeHeading: Aktuelle Termine
ProfileHeading: Benutzerprofil und Einstellungen ProfileHeading: Benutzerprofil und Einstellungen
@ -107,4 +126,20 @@ SheetSolution: Lösung
SheetMarking: Korrekturhinweise SheetMarking: Korrekturhinweise
MultiFileUploadInfo: (Mehrere Dateien mit Shift oder Strg auswählen) MultiFileUploadInfo: (Mehrere Dateien mit Shift oder Strg auswählen)
NrColumn: Nr
SelectColumn: Auswahl
CorrDownload: Herunterladen
CorrUploadField: Korrekturen
CorrUpload: Korrekturen hochladen
CorrSetCorrector: Korrektor zuweisen
CorrAutoSetCorrector: Korrekturen verteilen
NatField xyz@Text: #{xyz} muss eine natürliche Zahl sein! NatField xyz@Text: #{xyz} muss eine natürliche Zahl sein!
SubmissionsAlreadyAssigned num@Int64: #{display num} Abgaben waren bereits einem Korrektor zugeteilt und wurden nicht verändert:
UpdatedAssignedCorrectorSingle num@Int64: #{display num} Abgaben wurden dem neuen Korrektor zugeteilt.
NoCorrector: Kein Korrektor
RemovedCorrections num@Int64: Korrektur-Daten wurden von #{display num} Abgaben entfernt.
UpdatedAssignedCorrectorsAuto num@Int64: #{display num} Abgaben wurden unter den Korrektoren aufgeteilt.
CouldNotAssignCorrectorsAuto num@Int64: #{display num} Abgaben konnten nicht automatisch zugewiesen werden:

1
models
View File

@ -119,6 +119,7 @@ SheetCorrector
sheet SheetId sheet SheetId
load Load load Load
UniqueSheetCorrector user sheet UniqueSheetCorrector user sheet
deriving Show Eq Ord
SheetFile SheetFile
sheet SheetId sheet SheetId
file FileId file FileId

View File

@ -81,6 +81,7 @@ dependencies:
- exceptions - exceptions
- lens - lens
- MonadRandom - MonadRandom
- email-validate
# The library contains all of our application code. The executable # The library contains all of our application code. The executable
# defined below is just a thin wrapper. # defined below is just a thin wrapper.

22
routes
View File

@ -51,26 +51,24 @@
/show CShowR GET !free /show CShowR GET !free
/register CRegisterR POST !time /register CRegisterR POST !time
/edit CEditR GET POST /edit CEditR GET POST
/subs CourseCorrectionsR GET POST
/ex SheetListR GET !registered !materials /ex SheetListR GET !registered !materials
!/ex/new SheetNewR GET POST !/ex/new SheetNewR GET POST
/ex/#Text SheetR: /ex/#Text SheetR:
/show SShowR GET !timeANDregistered !timeANDmaterials !corrector /show SShowR GET !timeANDregistered !timeANDmaterials !corrector
!/#SheetFileType/*FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector
/edit SEditR GET POST /edit SEditR GET POST
/delete SDelR GET POST /delete SDelR GET POST
!/sub/new SubmissionNewR GET POST !timeANDregistered /sub/new SubmissionNewR GET POST !timeANDregistered
!/sub/own SubmissionOwnR GET !free /sub/own SubmissionOwnR GET !free
!/sub/#{ZIPArchiveName SubmissionId} SubmissionDownloadArchiveR GET !owner !corrector
!/sub/#CryptoFileNameSubmission SubmissionR GET POST !owner !corrector !/sub/#CryptoFileNameSubmission SubmissionR GET POST !owner !corrector
!/sub/#CryptoFileNameSubmission/*FilePath SubmissionDownloadSingleR GET !owner !corrector
/correctors SCorrR GET POST
/subs SSubsR GET POST
!/#SheetFileType/*FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector
/corrections CorrectionsR GET POST !free
-- TODO below
!/#{ZIPArchiveName SubmissionId} SubmissionDownloadArchiveR 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 !/#UUID CryptoUUIDDispatchR GET !free -- just redirect
!/*{CI FilePath} CryptoFileNameDispatchR GET !free -- !/*{CI FilePath} CryptoFileNameDispatchR GET !free -- Disabled until preliminary check for valid cID exists

View File

@ -48,6 +48,7 @@ import Handler.Term
import Handler.Course import Handler.Course
import Handler.Sheet import Handler.Sheet
import Handler.Submission import Handler.Submission
import Handler.Corrections
import Handler.CryptoIDDispatch import Handler.CryptoIDDispatch

View File

@ -39,6 +39,8 @@ import Data.ByteArray (convert)
import Crypto.Hash (Digest, SHAKE256) import Crypto.Hash (Digest, SHAKE256)
import Crypto.Hash.Conduit (sinkHash) import Crypto.Hash.Conduit (sinkHash)
import qualified Data.CryptoID (CryptoID) -- for DisplayAble instance only
import qualified Data.ByteString.Base64.URL as Base64 (encode) import qualified Data.ByteString.Base64.URL as Base64 (encode)
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
@ -72,13 +74,19 @@ import Handler.Utils.DateTime
import Control.Lens import Control.Lens
import Utils.Lens import Utils.Lens
-- -- TODO: Move me to appropriate Place
-- -- TODO: Move the following to the appropriate place, if DisplayAble is kept
instance DisplayAble TermId where instance DisplayAble TermId where
display = termToText . unTermKey display = termToText . unTermKey
instance DisplayAble UTCTime where instance DisplayAble UTCTime where
display = pack . formatTimeGerDT2 -- default Time Format to be used: 00.00.00 00:00 display = pack . formatTimeGerDT2 -- default Time Format to be used: 00.00.00 00:00
instance (PathPiece b) => DisplayAble (Data.CryptoID.CryptoID a b) where
display = toPathPiece -- requires import of Data.CryptoID here
-- -- MOVE ABOVE
-- infixl 9 :$: -- infixl 9 :$:
-- pattern a :$: b = a b -- pattern a :$: b = a b
@ -153,6 +161,9 @@ instance RenderMessage UniWorX TermIdentifier where
Winter -> renderMessage' $ MsgWinterTerm year Winter -> renderMessage' $ MsgWinterTerm year
where renderMessage' = renderMessage foundation ls where renderMessage' = renderMessage foundation ls
instance RenderMessage UniWorX String where
renderMessage f ls str = renderMessage f ls $ Text.pack str
instance RenderMessage UniWorX SheetFileType where instance RenderMessage UniWorX SheetFileType where
renderMessage foundation ls = \case renderMessage foundation ls = \case
SheetExercise -> renderMessage' MsgSheetExercise SheetExercise -> renderMessage' MsgSheetExercise
@ -565,7 +576,6 @@ instance Yesod UniWorX where
makeLogger = return . appLogger makeLogger = return . appLogger
-- Define breadcrumbs. -- Define breadcrumbs.
instance YesodBreadcrumbs UniWorX where instance YesodBreadcrumbs UniWorX where
breadcrumb (AuthR _) = return ("Login" , Just HomeR) breadcrumb (AuthR _) = return ("Login" , Just HomeR)
@ -595,14 +605,27 @@ instance YesodBreadcrumbs UniWorX where
-- (CSheetR tid csh shn SFileR) -- just for Downloads -- (CSheetR tid csh shn SFileR) -- just for Downloads
breadcrumb (CSheetR tid csh shn SEditR) = return ("Edit", Just $ CSheetR tid csh shn SShowR) breadcrumb (CSheetR tid csh shn SEditR) = return ("Edit", Just $ CSheetR tid csh shn SShowR)
breadcrumb (CSheetR tid csh shn SDelR ) = return ("DELETE", Just $ CSheetR tid csh shn SShowR) breadcrumb (CSheetR tid csh shn SDelR ) = return ("DELETE", Just $ CSheetR tid csh shn SShowR)
breadcrumb (CSheetR tid csh shn SCorrR) = return ("Korrektoren", Just $ CSheetR tid csh shn SShowR)
breadcrumb (CSheetR tid csh shn SSubsR) = return ("Abgaben", Just $ CSheetR tid csh shn SShowR)
breadcrumb (CSheetR tid csh shn SubmissionNewR) = return ("Abgabe", Just $ CSheetR tid csh shn SShowR) breadcrumb (CSheetR tid csh shn SubmissionNewR) = return ("Abgabe", Just $ CSheetR tid csh shn SShowR)
breadcrumb (CSheetR tid csh shn SubmissionOwnR) = return ("Abgabe", Just $ CSheetR tid csh shn SShowR) breadcrumb (CSheetR tid csh shn SubmissionOwnR) = return ("Abgabe", Just $ CSheetR tid csh shn SShowR)
breadcrumb (CSheetR tid csh shn (SubmissionR _)) = return ("Abgabe", Just $ CSheetR tid csh shn SShowR) breadcrumb (CSheetR tid csh shn (SubmissionR _)) = return ("Abgabe", Just $ CSheetR tid csh shn SShowR)
-- Deprecated below
breadcrumb SubmissionListR = return ("Abgaben", Just HomeR)
-- Others -- Others
breadcrumb _ = return ("Uni2work", Nothing) -- Default is no breadcrumb at all breadcrumb _ = return ("Uni2work", Nothing) -- Default is no breadcrumb at all
submissionList :: TermId -> Text -> Text -> UserId -> DB [E.Value SubmissionId]
submissionList tid csh shn uid = E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` submissionUser) -> do
E.on $ submissionUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
E.on $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val uid
E.&&. sheet E.^. SheetName E.==. E.val shn
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. course E.^. CourseTerm E.==. E.val tid
return $ submission E.^. SubmissionId
pageActions :: Route UniWorX -> [MenuTypes] pageActions :: Route UniWorX -> [MenuTypes]
pageActions (CourseR tid csh CShowR) = pageActions (CourseR tid csh CShowR) =
[ PageActionPrime $ MenuItem [ PageActionPrime $ MenuItem
@ -627,6 +650,12 @@ pageActions (CourseR tid csh CShowR) =
return (sheets,lecturer) return (sheets,lecturer)
or2M (return lecturer) $ anyM sheets sheetRouteAccess or2M (return lecturer) $ anyM sheets sheetRouteAccess
} }
, PageActionPrime $ MenuItem
{ menuItemLabel = "Abgaben"
, menuItemIcon = Nothing
, menuItemRoute = CourseR tid csh CourseCorrectionsR
, menuItemAccessCallback' = return True
}
, PageActionSecondary $ MenuItem , PageActionSecondary $ MenuItem
{ menuItemLabel = "Neues Übungsblatt anlegen" { menuItemLabel = "Neues Übungsblatt anlegen"
, menuItemIcon = Nothing , menuItemIcon = Nothing
@ -647,13 +676,33 @@ pageActions (CSheetR tid csh shn SShowR) =
{ menuItemLabel = "Abgabe anlegen" { menuItemLabel = "Abgabe anlegen"
, menuItemIcon = Nothing , menuItemIcon = Nothing
, menuItemRoute = CSheetR tid csh shn SubmissionNewR , menuItemRoute = CSheetR tid csh shn SubmissionNewR
, menuItemAccessCallback' = return True -- TODO: check that no submission already exists , menuItemAccessCallback' = runDB . maybeT (return False) $ do
uid <- MaybeT $ liftHandlerT maybeAuthId
submissions <- lift $ submissionList tid csh shn uid
guard $ null submissions
return True
} }
, PageActionPrime $ MenuItem , PageActionPrime $ MenuItem
{ menuItemLabel = "Abgabe ansehen" { menuItemLabel = "Abgabe ansehen"
, menuItemIcon = Nothing , menuItemIcon = Nothing
, menuItemRoute = CSheetR tid csh shn SubmissionOwnR , menuItemRoute = CSheetR tid csh shn SubmissionOwnR
, menuItemAccessCallback' = return True -- TODO: check that a submission already exists , menuItemAccessCallback' = runDB . maybeT (return False) $ do
uid <- MaybeT $ liftHandlerT maybeAuthId
submissions <- lift $ submissionList tid csh shn uid
guard . not $ null submissions
return True
}
, PageActionPrime $ MenuItem
{ menuItemLabel = "Korrektoren"
, menuItemIcon = Nothing
, menuItemRoute = CSheetR tid csh shn SCorrR
, menuItemAccessCallback' = return True
}
, PageActionPrime $ MenuItem
{ menuItemLabel = "Abgaben"
, menuItemIcon = Nothing
, menuItemRoute = CSheetR tid csh shn SSubsR
, menuItemAccessCallback' = return True
} }
] ]
pageActions TermShowR = pageActions TermShowR =
@ -697,7 +746,7 @@ pageActions (HomeR) =
-- , -- ,
NavbarAside $ MenuItem NavbarAside $ MenuItem
{ menuItemLabel = "AdminDemo" { menuItemLabel = "AdminDemo"
, menuItemIcon = Nothing , menuItemIcon = Just "book"
, menuItemRoute = AdminTestR , menuItemRoute = AdminTestR
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }
@ -741,6 +790,8 @@ pageHeading (CourseR tid csh CShowR)
= Just $ do = Just $ do
Entity _ Course{..} <- handlerToWidget . runDB . getBy404 $ CourseTermShort tid csh Entity _ Course{..} <- handlerToWidget . runDB . getBy404 $ CourseTermShort tid csh
toWidget courseName toWidget courseName
pageHeading CorrectionsR
= Just $ i18nHeading MsgCorrectionsTitle
-- (CourseR tid csh CRegisterR) -- just for POST -- (CourseR tid csh CRegisterR) -- just for POST
pageHeading (CourseR tid csh CEditR) pageHeading (CourseR tid csh CEditR)
= Just $ i18nHeading $ MsgCourseEditHeading tid csh = Just $ i18nHeading $ MsgCourseEditHeading tid csh

342
src/Handler/Corrections.hs Normal file
View File

@ -0,0 +1,342 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE MultiWayIf, LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RecordWildCards, NamedFieldPuns, TupleSections #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Handler.Corrections where
import Import
-- import System.FilePath (takeFileName)
import Handler.Utils
import Handler.Utils.Submission
-- import Handler.Utils.Zip
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Map (Map)
import qualified Data.Map as Map
-- import Data.Time
-- import qualified Data.Text as T
-- import Data.Function ((&))
--
import Colonnade hiding (fromMaybe, singleton)
-- import Yesod.Colonnade
--
-- import qualified Data.UUID.Cryptographic as UUID
-- import qualified Data.Conduit.List as C
import qualified Database.Esqueleto as E
-- import qualified Database.Esqueleto.Internal.Sql as E
import Control.Lens
-- import Control.Monad.Writer (MonadWriter(..), execWriterT)
-- import Network.Mime
import Web.PathPieces
import Text.Hamlet (ihamletFile)
import Text.Blaze.Html (preEscapedToHtml)
import Database.Persist.Sql (updateWhereCount)
import Data.List (genericLength)
type CorrectionsWhere = forall query expr backend . (E.Esqueleto query expr backend) =>
(expr (Entity Course), expr (Entity Sheet), expr (Entity Submission))
-> expr (E.Value Bool)
ratedBy :: Key User -> CorrectionsWhere
ratedBy uid (_course,_sheet,submission) = submission E.^. SubmissionRatingBy E.==. E.just (E.val uid)
courseIs :: Key Course -> CorrectionsWhere
courseIs cid (course,_sheet,_submission) = course E.^. CourseId E.==. E.val cid
sheetIs :: Key Sheet -> CorrectionsWhere
sheetIs shid (_course,sheet,_submission) = sheet E.^. SheetId E.==. E.val shid
type CorrectionTableData = DBRow (Entity Submission, Entity Sheet, (E.Value Text, E.Value Text, E.Value (Key Term), E.Value (Key School)), Maybe (Entity User))
colTerm :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colTerm = sortable (Just "term") (i18nCell MsgTerm)
$ \DBRow{ dbrOutput=(_, _, course, _) } ->
-- cell [whamlet| _{untermKey $ course ^. _3}|] -- lange, internationale Semester
textCell $ termToText $ unTermKey $ E.unValue $ course ^. _3 -- kurze Semsterkürzel
colCourse :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colCourse = sortable (Just "course") (i18nCell MsgCourse)
$ \DBRow{ dbrOutput=(_, _, course, _) } -> cell $
let tid = E.unValue $ course ^. _3
csh = E.unValue $ course ^. _2
in [whamlet|<a href=@{CourseR tid csh CShowR}>#{display csh}|]
colSheet :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colSheet = sortable (Just "sheet") (i18nCell MsgSheet)
$ \DBRow{ dbrOutput=(_, sheet, course, _) } -> cell $
let tid = E.unValue $ course ^. _3
csh = E.unValue $ course ^. _2
shn = sheetName $ entityVal sheet
in [whamlet|<a href=@{CSheetR tid csh shn SShowR}>#{display shn}|]
-- textCell $ sheetName $ entityVal sheet
colCorrector :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colCorrector = sortable (Just "corrector") (i18nCell MsgCorrector) $ \case
DBRow{ dbrOutput = (_, _, _, Nothing) } -> cell mempty
DBRow{ dbrOutput = (_, _, _, Just corr) } -> textCell . display . userDisplayName $ entityVal corr
colSubmissionLink :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colSubmissionLink = sortable Nothing (i18nCell MsgSubmission)
$ \DBRow{ dbrOutput=(submission, sheet, course, _) } -> cell $ do
let tid = E.unValue $ course ^. _3
csh = E.unValue $ course ^. _2
shn = sheetName $ entityVal sheet
cid <- encrypt (entityKey submission :: SubmissionId)
[whamlet|<a href=@{CSheetR tid csh shn (SubmissionR cid)}>#{display cid}|]
colSelect :: Colonnade _ CorrectionTableData (DBCell _ (FormResult (DBFormResult CorrectionTableData CryptoFileNameSubmission Bool)))
colSelect = dbSelect id $ \DBRow{ dbrOutput=(Entity subId _, _, _, _) } -> encrypt subId
type CorrectionTableExpr = (E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity Submission)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User))
makeCorrectionsTable :: ( IsDBTable m x, DBOutput CorrectionTableData r', ToSortable h, Functor h )
=> _ -> Colonnade h r' (DBCell m x) -> PSValidator m x -> Handler (DBResult m x)
makeCorrectionsTable whereClause colChoices psValidator = do
let tableData :: CorrectionTableExpr -> E.SqlQuery _
tableData ((course `E.InnerJoin` sheet `E.InnerJoin` submission) `E.LeftOuterJoin` corrector) = do
E.on $ corrector E.?. UserId E.==. submission E.^. SubmissionRatingBy
E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.where_ $ whereClause (course,sheet,submission)
let crse = ( course E.^. CourseName :: E.SqlExpr (E.Value Text)
, course E.^. CourseShorthand
, course E.^. CourseTerm
, course E.^. CourseSchool :: E.SqlExpr (E.Value SchoolId)
)
return (submission, sheet, crse, corrector)
dbTable psValidator $ DBTable
{ dbtSQLQuery = tableData
, dbtColonnade = colChoices
, dbtSorting = [ ( "term"
, SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseTerm
)
, ( "course"
, SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseShorthand
)
, ( "sheet"
, SortColumn $ \((_ `E.InnerJoin` sheet `E.InnerJoin` _) `E.LeftOuterJoin` _) -> sheet E.^. SheetName
)
, ( "corrector"
, SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` corrector) -> corrector E.?. UserDisplayName
)
]
, dbtFilter = [ ( "term"
, FilterColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _ :: CorrectionTableExpr) tids -> if
| Set.null tids -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> course E.^. CourseTerm `E.in_` E.valList (Set.toList tids)
)
, ( "course"
, FilterColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _ :: CorrectionTableExpr) cshs -> if
| Set.null cshs -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> course E.^. CourseShorthand `E.in_` E.valList (Set.toList cshs)
)
, ( "sheet"
, FilterColumn $ \((_ `E.InnerJoin` sheet `E.InnerJoin` _) `E.LeftOuterJoin` _ :: CorrectionTableExpr) shns -> if
| Set.null shns -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> sheet E.^. SheetName `E.in_` E.valList (Set.toList shns)
)
, ( "corrector"
, FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` corrector :: CorrectionTableExpr) emails -> if
| Set.null emails -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> corrector E.?. UserEmail `E.in_` E.justList (E.valList . catMaybes $ Set.toList emails)
E.||. (if Nothing `Set.member` emails then E.isNothing (corrector E.?. UserEmail) else E.val False)
)
]
, dbtAttrs = tableDefault
, dbtIdent = "corrections" :: Text
}
data ActionCorrections = CorrDownload
| CorrSetCorrector
| CorrAutoSetCorrector
deriving (Eq, Ord, Read, Show, Enum, Bounded)
instance PathPiece ActionCorrections where
fromPathPiece = readFromPathPiece
toPathPiece = showToPathPiece
instance RenderMessage UniWorX ActionCorrections where
renderMessage m ls CorrDownload = renderMessage m ls MsgCorrDownload
renderMessage m ls CorrSetCorrector = renderMessage m ls MsgCorrSetCorrector
renderMessage m ls CorrAutoSetCorrector = renderMessage m ls MsgCorrAutoSetCorrector
data ActionCorrectionsData = CorrDownloadData
| CorrSetCorrectorData (Maybe UserId)
| CorrAutoSetCorrectorData SheetId
correctionsR :: _ -> _ -> _ -> Map ActionCorrections (MForm (HandlerT UniWorX IO) (FormResult ActionCorrectionsData, Maybe Widget)) -> Handler TypedContent
correctionsR whereClause (formColonnade -> displayColumns) psValidator actions = do
tableForm <- makeCorrectionsTable whereClause displayColumns psValidator
((actionRes, table), tableEncoding) <- runFormPost . identForm FIDcorrectorTable $ \csrf -> do
((fmap (Map.keysSet . Map.filter id . getDBFormResult (const False)) -> selectionRes), table) <- tableForm csrf
(actionRes, action) <- multiAction actions
return ((,) <$> actionRes <*> selectionRes, table <> action)
Just currentRoute <- getCurrentRoute -- This should never be called from a 404 handler
case actionRes of
FormFailure errs -> mapM_ (addMessage "danger" . toHtml) errs
FormMissing -> return ()
FormSuccess (CorrDownloadData, subs) -> do
ids <- Set.fromList <$> forM (Set.toList subs) decrypt -- Set is not traversable
addHeader "Content-Disposition" "attachment; filename=\"corrections.zip\""
sendResponse =<< submissionMultiArchive ids
FormSuccess (CorrSetCorrectorData (Just uid), subs') -> do
subs <- mapM decrypt $ Set.toList subs'
runDB $ do
alreadyAssigned <- selectList [SubmissionId <-. subs, SubmissionRatingBy !=. Nothing] []
when (not $ null alreadyAssigned) $ do
mr <- (toHtml . ) <$> getMessageRender
alreadyAssigned' <- forM alreadyAssigned $ \Entity{..} -> (, entityVal) <$> (encrypt entityKey :: DB CryptoFileNameSubmission)
addMessage "warn" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsAlreadyAssigned.hamlet") mr)
let unassigned = Set.fromList subs `Set.difference` Set.fromList (entityKey <$> alreadyAssigned)
when (not $ null unassigned) $ do
num <- updateWhereCount [SubmissionId <-. Set.toList unassigned] [SubmissionRatingBy =. Just uid]
addMessageI "success" $ MsgUpdatedAssignedCorrectorSingle num
redirect currentRoute
FormSuccess (CorrSetCorrectorData Nothing, subs') -> do
subs <- mapM decrypt $ Set.toList subs'
runDB $ do
num <- updateWhereCount [SubmissionId <-. subs] [ SubmissionRatingPoints =. Nothing
, SubmissionRatingComment =. Nothing
, SubmissionRatingBy =. Nothing
, SubmissionRatingTime =. Nothing
]
addMessageI "success" $ MsgRemovedCorrections num
redirect currentRoute
FormSuccess (CorrAutoSetCorrectorData shid, subs') -> do
subs <- mapM decrypt $ Set.toList subs'
runDB $ do
alreadyAssigned <- selectList [SubmissionId <-. subs, SubmissionRatingBy !=. Nothing] []
when (not $ null alreadyAssigned) $ do
mr <- (toHtml . ) <$> getMessageRender
alreadyAssigned' <- forM alreadyAssigned $ \Entity{..} -> (, entityVal) <$> (encrypt entityKey :: DB CryptoFileNameSubmission)
addMessage "warn" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsAlreadyAssigned.hamlet") mr)
let unassigned = Set.fromList subs `Set.difference` Set.fromList (entityKey <$> alreadyAssigned)
when (not $ null unassigned) $ do
(assigned, unassigned) <- assignSubmissions shid (Just unassigned)
when (not $ null assigned) $
addMessageI "success" $ MsgUpdatedAssignedCorrectorsAuto (fromIntegral $ Set.size assigned)
when (not $ null unassigned) $ do
mr <- (toHtml . ) <$> getMessageRender
unassigned' <- forM (Set.toList unassigned) $ \sid -> (encrypt sid :: DB CryptoFileNameSubmission)
addMessage "warn" =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsNotAssignedAuto.hamlet") mr)
redirect currentRoute
fmap toTypedContent . defaultLayout $ do
setTitleI MsgCourseCorrectionsTitle
$(widgetFile "corrections")
type ActionCorrections' = (ActionCorrections, MForm (HandlerT UniWorX IO) (FormResult ActionCorrectionsData, Maybe Widget))
downloadAction :: ActionCorrections'
downloadAction = ( CorrDownload
, return (pure CorrDownloadData, Nothing)
)
assignAction :: Either CourseId SheetId -> ActionCorrections'
assignAction selId = ( CorrSetCorrector
, over (mapped._2) Just $ do
correctors <- liftHandlerT . runDB . E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector `E.InnerJoin` user) -> do
E.on $ user E.^. UserId E.==. sheetCorrector E.^. SheetCorrectorUser
E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.where_ $ either (\cId -> course E.^. CourseId E.==. E.val cId) (\shId -> sheet E.^. SheetId E.==. E.val shId) selId
return user
mr <- getMessageRender
correctors' <- fmap ((mr MsgNoCorrector, Nothing) :) . forM correctors $ \Entity{ entityKey, entityVal = User{..} } -> (display userDisplayName, ) . Just <$> encrypt entityKey
($ mempty) . renderAForm FormStandard . wFormToAForm $ do
cId <- wreq (selectFieldList correctors' :: Field (HandlerT UniWorX IO) (Maybe CryptoUUIDUser)) (fslI MsgCorrector) Nothing
fmap CorrSetCorrectorData <$> (traverse.traverse) decrypt cId
)
autoAssignAction :: SheetId -> ActionCorrections'
autoAssignAction shid = ( CorrAutoSetCorrector
, return (pure $ CorrAutoSetCorrectorData shid, Nothing)
)
getCorrectionsR, postCorrectionsR :: Handler TypedContent
getCorrectionsR = postCorrectionsR
postCorrectionsR = do
uid <- requireAuthId
let whereClause = ratedBy uid
colonnade = mconcat
[ colSelect
, dbRow
, colTerm
, colCourse
, colSheet
, colSubmissionLink
] -- Continue here
psValidator = def
& restrictFilter (\name _ -> name /= "corrector") -- We need to be careful to restrict allowed sorting/filter to not expose sensitive information
& restrictSorting (\name _ -> name /= "corrector")
correctionsR whereClause colonnade psValidator $ Map.fromList
[ downloadAction
]
getCourseCorrectionsR, postCourseCorrectionsR :: TermId -> Text -> Handler TypedContent
getCourseCorrectionsR = postCourseCorrectionsR
postCourseCorrectionsR tid csh = do
Entity cid _ <- runDB $ getBy404 $ CourseTermShort tid csh
let whereClause = courseIs cid
colonnade = mconcat
[ colSelect
, dbRow
, colSheet
, colCorrector
, colSubmissionLink
] -- Continue here
psValidator = def
correctionsR whereClause colonnade psValidator $ Map.fromList
[ downloadAction
, assignAction (Left cid)
]
getSSubsR, postSSubsR :: TermId -> Text -> Text -> Handler TypedContent
getSSubsR = postSSubsR
postSSubsR tid csh shn = do
shid <- runDB $ fetchSheetId tid csh shn
let whereClause = sheetIs shid
colonnade = mconcat
[ colSelect
, dbRow
, colCorrector
, colSubmissionLink
]
psValidator = def
correctionsR whereClause colonnade psValidator $ Map.fromList
[ downloadAction
, assignAction (Right shid)
, autoAssignAction shid
]

View File

@ -24,7 +24,7 @@ import Data.Time
-- import Control.Lens -- import Control.Lens
import Colonnade hiding (fromMaybe, singleton) import Colonnade hiding (fromMaybe, singleton)
import Yesod.Colonnade -- import Yesod.Colonnade
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
-- import qualified Data.UUID.Cryptographic as UUID -- import qualified Data.UUID.Cryptographic as UUID
@ -68,10 +68,10 @@ homeAnonymous = do
E.limit nrSheetDeadlines E.limit nrSheetDeadlines
return course return course
colonnade :: Colonnade Sortable (DBRow (Entity Course)) (Cell UniWorX) colonnade :: Colonnade Sortable (DBRow (Entity Course)) (DBCell (WidgetT UniWorX IO) ())
colonnade = mconcat colonnade = mconcat
[ -- dbRow [ -- dbRow
sortable (Just "course") (i18nCell MsgCourse) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> do sortable (Just "course") (textCell MsgCourse) $ \DBRow{ dbrOutput=(Entity {entityVal = course}) } -> do
let tid = courseTerm course let tid = courseTerm course
csh = courseShorthand course csh = courseShorthand course
cell [whamlet|<a href=@{CourseR tid csh CShowR}>#{display csh}|] cell [whamlet|<a href=@{CourseR tid csh CShowR}>#{display csh}|]
@ -134,17 +134,17 @@ homeUser uid = do
, sheet E.^. SheetActiveTo , sheet E.^. SheetActiveTo
) )
colonnade :: Colonnade Sortable (DBRow (E.Value (Key Term), E.Value Text, E.Value Text, E.Value UTCTime)) (Cell UniWorX) colonnade :: Colonnade Sortable (DBRow (E.Value (Key Term), E.Value Text, E.Value Text, E.Value UTCTime)) (DBCell (WidgetT UniWorX IO) ())
colonnade = mconcat colonnade = mconcat
[ -- dbRow [ -- dbRow
sortable (Just "course") (i18nCell MsgCourse) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, _, _) } -> sortable (Just "course") (textCell MsgCourse) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, _, _) } ->
cell [whamlet|<a href=@{CourseR tid csh CShowR}>#{display csh}|] cell [whamlet|<a href=@{CourseR tid csh CShowR}>#{display csh}|]
, sortable (Just "sheet") (i18nCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value csh, E.Value shn, _) } -> , 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}|] cell [whamlet|<a href=@{CSheetR tid csh shn SShowR}>#{display shn}|]
, sortable (Just "deadline") (i18nCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, E.Value deadline) } -> , sortable (Just "deadline") (textCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, E.Value deadline) } ->
textCell $ display deadline textCell $ display deadline
, sortable (Just "done") (i18nCell MsgDone) $ \DBRow{ dbrOutput=(_, _, _, _) } -> , sortable (Just "done") (textCell MsgDone) $ \DBRow{ dbrOutput=(_, _, _, _) } ->
textCell $ "?" textCell ("?" :: Text)
] ]
sheetTable <- dbTable def $ DBTable sheetTable <- dbTable def $ DBTable
{ dbtSQLQuery = tableData { dbtSQLQuery = tableData

View File

@ -31,7 +31,7 @@ makeSettingForm template = identForm FIDsettings $ \html -> do
<$> areq (natFieldI $ MsgNatField "Favoriten") -- TODO: natFieldI not working here <$> areq (natFieldI $ MsgNatField "Favoriten") -- TODO: natFieldI not working here
(fslpI MsgFavoriten "Anzahl Favoriten") (stgMaxFavourties <$> template) (fslpI MsgFavoriten "Anzahl Favoriten") (stgMaxFavourties <$> template)
<*> areq (selectFieldList themeList) <*> areq (selectFieldList themeList)
(fslI MsgTheme ) (stgTheme <$> template) (fslpI MsgTheme "theme-select" ) (stgTheme <$> template) -- TODO: pass theme-select as id-attribute or similar.
<* submitButton <* submitButton
return (result, widget) -- no validation required here return (result, widget) -- no validation required here
@ -70,7 +70,7 @@ getProfileR = do
E.where_ $ adright ^. UserAdminUser E.==. E.val uid E.where_ $ adright ^. UserAdminUser E.==. E.val uid
E.on $ adright ^. UserAdminSchool E.==. school ^. SchoolId E.on $ adright ^. UserAdminSchool E.==. school ^. SchoolId
return (school ^. SchoolShorthand) return (school ^. SchoolShorthand)
) )
<*> <*>
(E.select $ E.from $ \(lecright `E.InnerJoin` school) -> do (E.select $ E.from $ \(lecright `E.InnerJoin` school) -> do
E.where_ $ lecright ^. UserLecturerUser E.==. E.val uid E.where_ $ lecright ^. UserLecturerUser E.==. E.val uid

View File

@ -8,7 +8,12 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE MultiWayIf, LambdaCase #-}
{-# LANGUAGE TupleSections #-}
module Handler.Sheet where module Handler.Sheet where
@ -22,9 +27,10 @@ import Handler.Utils.Zip
import qualified Data.Text as T import qualified Data.Text as T
-- import Data.Function ((&)) -- import Data.Function ((&))
-- --
import Colonnade hiding (fromMaybe, singleton) import Colonnade hiding (fromMaybe, singleton, bool)
import Yesod.Colonnade import qualified Yesod.Colonnade as Yesod
-- import Text.Blaze (text)
--
import qualified Data.UUID.Cryptographic as UUID import qualified Data.UUID.Cryptographic as UUID
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
@ -32,12 +38,24 @@ import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E import qualified Database.Esqueleto.Internal.Sql as E
import Control.Monad.Writer (MonadWriter(..), execWriterT) import Control.Monad.Writer (MonadWriter(..), execWriterT)
import Control.Monad.Trans.RWS.Lazy (RWST, local)
import qualified Text.Email.Validate as Email
import qualified Data.List as List
import Network.Mime import Network.Mime
import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Map (Map, (!), (!?))
import qualified Data.Map as Map
import Control.Lens
import Utils.Lens
instance Eq (Unique Sheet) where instance Eq (Unique Sheet) where
(CourseSheet cid1 name1) == (CourseSheet cid2 name2) = (CourseSheet cid1 name1) == (CourseSheet cid2 name2) =
@ -64,7 +82,6 @@ data SheetForm = SheetForm
, sfSolutionFrom :: Maybe UTCTime , sfSolutionFrom :: Maybe UTCTime
, sfSolutionF :: Maybe FileInfo , sfSolutionF :: Maybe FileInfo
-- Keine SheetId im Formular! -- Keine SheetId im Formular!
, sfCorrectors :: [(UserId,Load)]
} }
@ -92,7 +109,6 @@ makeSheetForm msId template = identForm FIDsheet $ \html -> do
<*> fileAFormOpt (fsb "Hinweis") <*> fileAFormOpt (fsb "Hinweis")
<*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template) <*> aopt utcTimeField (fsb "Lösung ab") (sfSolutionFrom <$> template)
<*> fileAFormOpt (fsb "Lösung") <*> fileAFormOpt (fsb "Lösung")
<*> formToAForm (correctorForm msId (maybe [] sfCorrectors template))
<* submitButton <* submitButton
return $ case result of return $ case result of
FormSuccess sheetResult FormSuccess sheetResult
@ -123,16 +139,6 @@ makeSheetForm msId template = identForm FIDsheet $ \html -> do
-- TODO: continue validation here!!! -- TODO: continue validation here!!!
] ] ] ]
correctorForm :: Maybe SheetId -> [(UserId,Load)] -> MForm Handler (FormResult [(UserId,Load)], [FieldView UniWorX])
correctorForm _msid templates = return mempty -- TODO deprecated
-- Datenbank UserId -> UserName
-- Eingabelist für Colonnade
-- enthält die benötigten Felder
-- FormResult konstruieren
-- Eingabebox für Korrektor hinzufügen
-- Eingabe für Korrekt ausgefüllt: FormMissing zurückschicken um dann Feld hinzuzufügen
-- List Sheets -- List Sheets
getSheetListCID :: CourseId -> Handler Html getSheetListCID :: CourseId -> Handler Html
getSheetListCID cid = getSheetList =<< getSheetListCID cid = getSheetList =<<
@ -180,7 +186,7 @@ getSheetList courseEnt = do
setTitle $ toHtml $ csh <> " Übungsblätter" setTitle $ toHtml $ csh <> " Übungsblätter"
if null sheets if null sheets
then [whamlet|Es wurden noch keine Übungsblätter angelegt.|] then [whamlet|Es wurden noch keine Übungsblätter angelegt.|]
else encodeWidgetTable tableDefault colSheets sheets else Yesod.encodeWidgetTable tableDefault colSheets sheets
-- Show single sheet -- Show single sheet
@ -211,8 +217,8 @@ getSShowR tid csh shn = do
E.&&. E.not_ (E.isNothing $ file E.^. FileContent) E.&&. E.not_ (E.isNothing $ file E.^. FileContent)
-- return desired columns -- return desired columns
return $ (file E.^. FileTitle, file E.^. FileModified, sheetFile E.^. SheetFileType) return $ (file E.^. FileTitle, file E.^. FileModified, sheetFile E.^. SheetFileType)
let colonnadeFiles = mconcat let colonnadeFiles = widgetColonnade $ mconcat
[ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> cell $ [whamlet| _{ftype}|] [ 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)) , sortable (Just "path") "Dateiname" $ anchorCell (\(E.Value fName,_,E.Value fType) -> CSheetR tid csh shn (SFileR fType fName))
(\(E.Value fName,_,_) -> str2widget fName) (\(E.Value fName,_,_) -> str2widget fName)
, sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> stringCell $ formatTimeGerWDT (modified :: UTCTime) , sortable (Just "time") "Modifikation" $ \(_,E.Value modified,_) -> stringCell $ formatTimeGerWDT (modified :: UTCTime)
@ -303,7 +309,6 @@ getSEditR tid csh shn = do
, sfHintF = Nothing -- TODO , sfHintF = Nothing -- TODO
, sfSolutionFrom = sheetSolutionFrom , sfSolutionFrom = sheetSolutionFrom
, sfSolutionF = Nothing -- TODO , sfSolutionF = Nothing -- TODO
, sfCorrectors = [] -- TODO read correctors from list
} }
let action newSheet = do let action newSheet = do
replaceRes <- myReplaceUnique sid $ newSheet replaceRes <- myReplaceUnique sid $ newSheet
@ -410,3 +415,201 @@ insertSheetFile' sid ftype fs = do
finsert (Right file) = lift $ do finsert (Right file) = lift $ do
fid <- insert file fid <- insert file
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step
data CorrectorForm = CorrectorForm
{ cfUserId :: UserId
, cfUserName :: Text
, cfResult :: FormResult Load
, cfViewByTut, cfViewProp, cfViewDel :: FieldView UniWorX
}
type Loads = Map UserId Load
defaultLoads :: SheetId -> DB Loads
-- ^ Generate `Loads` in such a way that minimal editing is required
--
-- For every user, that ever was a corrector for this course, return their last `Load`.
-- "Last `Load`" is taken to mean their `Load` on the `Sheet` with the most recent creation time (first edit).
defaultLoads shid = do
cId <- sheetCourse <$> getJust shid
fmap toMap . E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> E.distinctOnOrderBy [E.asc (sheetCorrector E.^. SheetCorrectorUser)] $ do
E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet
let creationTime = E.sub_select . E.from $ \sheetEdit -> do
E.where_ $ sheetEdit E.^. SheetEditSheet E.==. sheet E.^. SheetId
return . E.min_ $ sheetEdit E.^. SheetEditTime
E.where_ $ sheet E.^. SheetCourse E.==. E.val cId
E.orderBy [E.desc creationTime]
return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad)
where
toMap :: [(E.Value UserId, E.Value Load)] -> Loads
toMap = foldMap $ \(E.Value uid, E.Value load) -> Map.singleton uid load
correctorForm :: SheetId -> MForm Handler (FormResult (Set SheetCorrector), [FieldView UniWorX])
correctorForm shid = do
cListIdent <- newFormIdent
let
guardNonDeleted :: UserId -> Handler (Maybe UserId)
guardNonDeleted uid = do
cID@CryptoID{..} <- encrypt uid :: Handler CryptoUUIDUser
deleted <- lookupPostParam $ tshow ciphertext <> "-" <> "del"
return $ bool Just (const Nothing) (isJust deleted) uid
formCIDs <- mapM decrypt =<< catMaybes <$> liftHandlerT (map fromPathPiece <$> lookupPostParams cListIdent :: Handler [Maybe CryptoUUIDUser])
let
currentLoads :: DB Loads
currentLoads = Map.fromList . map (\(Entity _ SheetCorrector{..}) -> (sheetCorrectorUser, sheetCorrectorLoad)) <$> selectList [ SheetCorrectorSheet ==. shid ] []
(defaultLoads', currentLoads') <- lift . runDB $ (,) <$> defaultLoads shid <*> currentLoads
loads' <- fmap (Map.fromList [(uid, mempty) | uid <- formCIDs] `Map.union`) $ if
| Map.null currentLoads'
, null formCIDs -> defaultLoads' <$ when (not $ Map.null defaultLoads') (addMessageI "warn" MsgCorrectorsDefaulted)
| otherwise -> return $ Map.fromList (map (, mempty) formCIDs) `Map.union` currentLoads'
deletions <- lift $ foldM (\dels uid -> maybe (Set.insert uid dels) (const dels) <$> guardNonDeleted uid) Set.empty (Map.keys loads')
let loads'' = Map.restrictKeys loads' (Map.keysSet loads' `Set.difference` deletions)
didDelete = any (flip Set.member deletions) formCIDs
(countTutRes, countTutView) <- mreq checkBoxField (fsm MsgCountTutProp) . Just $ any (\Load{..} -> fromMaybe False byTutorial) $ Map.elems loads'
let
tutorField :: Field Handler [Text]
tutorField = multiEmailField
{ fieldView = \theId name attrs val isReq -> asWidgetT $ do
listIdent <- newIdent
userId <- handlerToWidget requireAuthId
previousCorrectors <- handlerToWidget . runDB . E.select . E.from $ \(user `E.InnerJoin` sheetCorrector `E.InnerJoin` sheet `E.InnerJoin` course `E.InnerJoin` lecturer) -> E.distinctOnOrderBy [E.asc $ user E.^. UserEmail ] $ do
E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet
E.on $ sheetCorrector E.^. SheetCorrectorUser E.==. user E.^. UserId
E.where_ $ lecturer E.^. LecturerUser E.==. E.val userId
return $ user E.^. UserEmail
[whamlet|
$newline never
<input id=#{theId} name=#{name} list=#{listIdent} *{attrs} type=email multiple :isReq:required value="" placeholder=_{MsgCorrectorsPlaceholder}>
<datalist id=#{listIdent}>
$forall E.Value prev <- previousCorrectors
<option value=#{prev}>
|]
}
(addTutRes, addTutView) <- mopt tutorField (fsm MsgAddCorrector) (Just Nothing)
loads <- case addTutRes of
FormSuccess (Just emails) -> fmap Map.unions . forM emails $ \email -> do
mUid <- fmap (fmap entityKey) . lift . runDB $ getBy (UniqueEmail email)
case mUid of
Nothing -> loads'' <$ addMessageI "error" (MsgEMailUnknown email)
Just uid
| not (Map.member uid loads') -> return $ Map.insert uid mempty loads''
| otherwise -> loads'' <$ addMessageI "warning" (MsgCorrectorExists email)
FormFailure errs -> loads'' <$ mapM_ (addMessage "error" . toHtml) errs
_ -> return loads''
let deletions' = deletions `Set.difference` Map.keysSet loads
names <- fmap (Map.fromList . map (\(E.Value a, E.Value b) -> (a, b))) . lift . runDB . E.select . E.from $ \user -> do
E.where_ $ user E.^. UserId `E.in_` E.valList (Map.keys loads)
return $ (user E.^. UserId, user E.^. UserDisplayName)
let
constructFields :: (UserId, Text, Load) -> MForm Handler CorrectorForm
constructFields (uid, uname, Load{..}) = do
cID@CryptoID{..} <- encrypt uid :: MForm Handler CryptoUUIDUser
let
fs name = ""
{ fsName = Just $ tshow ciphertext <> "-" <> name
}
rationalField = convertField toRational fromRational doubleField
(byTutRes, cfViewByTut) <- mreq checkBoxField (fs "bytut") (Just $ isJust byTutorial)
(propRes, cfViewProp) <- mreq (checkBool (>= 0) MsgProportionNegative $ rationalField) (fs "prop") (Just byProportion)
(_, cfViewDel) <- mreq checkBoxField (fs "del") (Just False)
let
cfResult :: FormResult Load
cfResult = Load <$> tutRes' <*> propRes
tutRes'
| FormSuccess True <- byTutRes = Just <$> countTutRes
| otherwise = Nothing <$ byTutRes
cfUserId = uid
cfUserName = uname
return CorrectorForm{..}
corrData <- sequence . catMaybes . (flip map) (Map.keys loads) $ \uid -> fmap constructFields $ (,,) <$> pure uid <*> names !? uid <*> loads !? uid
mr <- getMessageRender
$logDebugS "SCorrR" $ tshow (didDelete, addTutRes)
let
corrColonnade = mconcat
[ headed (Yesod.textCell $ mr MsgCorrector) $ \CorrectorForm{..} -> Yesod.textCell cfUserName
, headed (Yesod.textCell $ mr MsgCorByTut) $ \CorrectorForm{..} -> Yesod.cell $ fvInput cfViewByTut
, headed (Yesod.textCell $ mr MsgCorProportion) $ \CorrectorForm{..} -> Yesod.cell $ fvInput cfViewProp
, headed (Yesod.textCell $ mr MsgDeleteRow) $ \CorrectorForm{..} -> Yesod.cell $ fvInput cfViewDel
]
corrResults
| FormSuccess (Just es) <- addTutRes
, not $ null es = FormMissing
| didDelete = FormMissing
| otherwise = fmap Set.fromList $ sequenceA [ SheetCorrector <$> pure cfUserId <*> pure shid <*> cfResult
| CorrectorForm{..} <- corrData
]
idField CorrectorForm{..} = do
cID <- encrypt cfUserId :: WidgetT UniWorX IO CryptoUUIDUser
toWidget [hamlet|<input name=#{cListIdent} type=hidden value=#{toPathPiece cID}>|]
delField uid = do
cID <- encrypt uid :: WidgetT UniWorX IO CryptoUUIDUser
toWidget [hamlet|<input name="#{toPathPiece cID}-del" type=hidden value=yes>|]
return (corrResults, [ countTutView
, FieldView
{ fvLabel = text $ mr MsgCorrectors
, fvTooltip = Nothing
, fvId = ""
, fvInput = Yesod.encodeCellTable tableDefault corrColonnade corrData >> mapM_ idField corrData >> mapM_ delField deletions
, fvErrors = Nothing
, fvRequired = True
}
, addTutView
{ fvInput = [whamlet|
<div>
^{fvInput addTutView}
<button type=submit formnovalidate data-formnorequired>Hinzufügen
|]
}
])
-- Eingabebox für Korrektor hinzufügen
-- Eingabe für Korrekt ausgefüllt: FormMissing zurückschicken um dann Feld hinzuzufügen
getSCorrR, postSCorrR :: TermId
-> Text -- ^ Course shorthand
-> Text -- ^ Sheet name
-> Handler Html
postSCorrR = getSCorrR
getSCorrR tid@(unTermKey -> tident) csh shn = do
Entity shid Sheet{..} <- runDB $ fetchSheet tid csh shn
((res,formWidget), formEnctype) <- runFormPost . identForm FIDcorrectors . renderAForm FormStandard $ formToAForm (correctorForm shid) <* submitButton
case res of
FormFailure errs -> mapM_ (addMessage "error" . toHtml) errs
FormSuccess res -> runDB $ do
deleteWhere [SheetCorrectorSheet ==. shid]
insertMany_ $ Set.toList res
addMessageI "success" MsgCorrectorsUpdated
FormMissing -> return ()
let
formTitle = MsgSheetCorrectorsTitle tident csh shn
formText = Nothing :: Maybe (SomeMessage UniWorX)
actionUrl = CSheetR tid csh shn SCorrR
-- actionUrl = CSheetR tid csh shn SShowR
defaultLayout $ do
setTitleI $ MsgSheetCorrectorsTitle tident csh shn
$(widgetFile "formPageI18n")

View File

@ -13,6 +13,7 @@
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE PatternGuards #-} {-# LANGUAGE PatternGuards #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-# LANGUAGE PartialTypeSignatures #-}
module Handler.Submission where module Handler.Submission where
@ -49,7 +50,7 @@ import Data.Bifunctor
import System.FilePath import System.FilePath
import Colonnade hiding (bool) import Colonnade hiding (bool)
import Yesod.Colonnade import qualified Yesod.Colonnade as Yesod
import qualified Text.Blaze.Html5.Attributes as HA import qualified Text.Blaze.Html5.Attributes as HA
@ -204,7 +205,7 @@ submissionHelper tid csh shn (SubmissionMode mcid) = do
(Nothing, Just smid) -- no new files, existing submission partners updated (Nothing, Just smid) -- no new files, existing submission partners updated
-> return smid -> return smid
(Just files, _) -- new files (Just files, _) -- new files
-> runConduit $ transPipe lift files .| extractRatings .| sinkSubmission shid uid ((,False) <$> msmid) -> runConduit $ transPipe lift files .| extractRatings .| sinkSubmission uid (maybe (Left shid) Right msmid) False
_ -> error "Impossible, because of definition of `makeSubmissionForm`" _ -> error "Impossible, because of definition of `makeSubmissionForm`"
-- Determine members of pre-registered group -- Determine members of pre-registered group
groupUids <- fmap (setFromList . map E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser') -> do groupUids <- fmap (setFromList . map E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser') -> do
@ -236,9 +237,10 @@ submissionHelper tid csh shn (SubmissionMode mcid) = do
let formText = Nothing :: Maybe UniWorXMessage let formText = Nothing :: Maybe UniWorXMessage
actionUrl <- Data.Maybe.fromJust <$> getCurrentRoute actionUrl <- Data.Maybe.fromJust <$> getCurrentRoute
-- Maybe construct a table to display uploaded archive files -- Maybe construct a table to display uploaded archive files
let colonnadeFiles cid = mconcat let colonnadeFiles :: _ -> Colonnade Sortable _ (DBCell (WidgetT UniWorX IO) ())
colonnadeFiles cid = mconcat
-- [ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> textCell $ toPathPiece ftype -- [ sortable (Just "type") "Typ" $ \(_,_, E.Value ftype) -> textCell $ toPathPiece ftype
[ sortable (Just "path") "Dateiname" $ anchorCell (\(Entity _ File{..}) -> SubmissionDownloadSingleR cid fileTitle) [ sortable (Just "path") "Dateiname" $ anchorCell (\(Entity _ File{..}) -> CSheetR tid csh shn $ SubmissionDownloadSingleR cid fileTitle)
(\(Entity _ File{..}) -> str2widget fileTitle) (\(Entity _ File{..}) -> str2widget fileTitle)
, sortable (Just "time") "Modifikation" $ \(Entity _ File{..}) -> stringCell $ formatTimeGerWDT fileModified , sortable (Just "time") "Modifikation" $ \(Entity _ File{..}) -> stringCell $ formatTimeGerWDT fileModified
] ]
@ -265,7 +267,7 @@ submissionHelper tid csh shn (SubmissionMode mcid) = do
$maybe arCid <- mArCid $maybe arCid <- mArCid
<hr> <hr>
<h2> <h2>
<a href=@{SubmissionDownloadArchiveR arCid}>Archiv <a href=@{CSheetR tid csh shn (SubmissionDownloadArchiveR arCid)}>Archiv
$forall (name,time) <- lastEdits $forall (name,time) <- lastEdits
<div>last edited by #{name} at #{formatTimeGerDTlong time} <div>last edited by #{name} at #{formatTimeGerDTlong time}
$maybe fileTable <- mFileTable $maybe fileTable <- mFileTable
@ -274,26 +276,15 @@ submissionHelper tid csh shn (SubmissionMode mcid) = do
|] |]
getSubmissionDownloadSingleR :: TermId -> Text -> Text -> CryptoFileNameSubmission -> FilePath -> Handler TypedContent
getSubmissionDownloadSingleR tid csh shn cID path = do
submissionFileSource :: SubmissionId -> Source (YesodDB UniWorX) (Entity File)
submissionFileSource = E.selectSource . E.from . submissionFileQuery
submissionFileQuery :: SubmissionId -> E.SqlExpr (Entity SubmissionFile) `E.InnerJoin` E.SqlExpr (Entity File)
-> E.SqlQuery (E.SqlExpr (Entity File))
submissionFileQuery submissionID (sf `E.InnerJoin` f) = E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
E.where_ . E.not_ $ sf E.^. SubmissionFileIsDeletion -- TODO@gk: won't work as intended! Fix with refactor
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] -- E.desc returns corrector updated data first
return f
getSubmissionDownloadSingleR :: CryptoFileNameSubmission -> FilePath -> Handler TypedContent
getSubmissionDownloadSingleR cID path = do
submissionID <- decrypt cID submissionID <- decrypt cID
runDB $ do runDB $ do
shid <- fetchSheetId tid csh shn
Submission{..} <- get404 submissionID
when (shid /= submissionSheet) $ invalidArgsI [MsgSubmissionWrongSheet]
isRating <- maybe False (== submissionID) <$> isRatingFile path isRating <- maybe False (== submissionID) <$> isRatingFile path
case isRating of case isRating of
True -> do True -> do
@ -311,202 +302,24 @@ getSubmissionDownloadSingleR cID path = do
let fileName = Text.pack $ takeFileName path let fileName = Text.pack $ takeFileName path
case results of case results of
[Entity _ File{ fileContent = Just c }] -> return $ TypedContent (defaultMimeLookup fileName) (toContent c) [Entity _ File{ fileContent = Just c }] -> return $ TypedContent (defaultMimeLookup fileName <> "; charset=utf-8") (toContent c)
_ -> notFound _ -> notFound
getSubmissionDownloadArchiveR :: ZIPArchiveName SubmissionId -> Handler TypedContent getSubmissionDownloadArchiveR :: TermId -> Text -> Text -> ZIPArchiveName SubmissionId -> Handler TypedContent
getSubmissionDownloadArchiveR (ZIPArchiveName cID) = do getSubmissionDownloadArchiveR tid csh shn (ZIPArchiveName cID) = do
submissionID <- decrypt cID submissionID <- decrypt cID
cUUID <- encrypt submissionID cUUID <- encrypt submissionID
respondSourceDB "application/zip" $ do respondSourceDB "application/zip" $ do
lift $ do
shid <- fetchSheetId tid csh shn
Submission{..} <- get404 submissionID
when (shid /= submissionSheet) $ invalidArgsI [MsgSubmissionWrongSheet]
rating <- lift $ getRating submissionID rating <- lift $ getRating submissionID
case rating of case rating of
Nothing -> lift notFound Nothing -> lift notFound
Just rating' -> do Just rating' -> do
let fileEntitySource' :: Source (YesodDB UniWorX) File let fileEntitySource' :: Source (YesodDB UniWorX) File
fileEntitySource' = submissionFileSource submissionID =$= Conduit.map entityVal >> yieldM (ratingFile cID rating') fileEntitySource' = submissionFileSource submissionID =$= Conduit.map entityVal >> yieldM (ratingFile cID rating')
info = ZipInfo { zipComment = Text.encodeUtf8 . tshow $ ciphertext (cUUID :: CryptoUUIDSubmission) } info = ZipInfo { zipComment = Text.encodeUtf8 . pack . CI.foldedCase $ ciphertext (cUUID :: CryptoFileNameSubmission) }
fileEntitySource' =$= produceZip info =$= Conduit.map toFlushBuilder fileEntitySource' =$= produceZip info =$= Conduit.map toFlushBuilder
-----------------------------------------------------------------------------------------------
------------------------- DEMO BELOW
submissionTable :: MForm Handler (FormResult [SubmissionId], Widget)
submissionTable = do
subs <- lift . runDB $ E.select . E.from $ \(sub `E.InnerJoin` sheet `E.InnerJoin` course) -> do
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.on $ sheet E.^. SheetId E.==. sub E.^. SubmissionSheet
return (sub, sheet, course)
cryptedSubs <- forM subs $ \s@(Entity submissionId _, _, _) ->
(,,) <$> encrypt submissionId <*> encrypt submissionId <*> pure s
let
anchorCourse (_, _, (_, _, Entity _ Course{..})) = CourseR courseTerm courseShorthand CShowR
courseText (_, _, (_, _, Entity _ Course{..})) = toWidget courseName
anchorSubmission (_, cUUID, _) = SubmissionDemoR cUUID
submissionText (cID, _, _) = toWidget . toPathPiece . CI.foldedCase $ ciphertext cID
colonnade = mconcat
[ headed "Abgabe-ID" $ anchorCell anchorSubmission submissionText
, headed "Kurs" $ anchorCell anchorCourse courseText
, headed "Blatt" $ \(_, _, (_, Entity _ Sheet{..}, _)) -> textCell $ sheetName
]
toExternal :: (CryptoFileNameSubmission, CryptoUUIDSubmission, a) -> Handler CryptoUUIDSubmission
toExternal (_, cID, _) = return cID
fromExternal :: CryptoUUIDSubmission -> Handler SubmissionId
fromExternal = decrypt
headedRowSelector toExternal fromExternal (HA.class_ "table table-striped table-hover") colonnade cryptedSubs
getSubmissionListR, postSubmissionListR :: Handler Html
getSubmissionListR = postSubmissionListR
postSubmissionListR = do
((uploadResult, uploadWidget), uploadEnctype) <- runFormPost . renderAForm FormStandard $ (,)
<$> areq checkBoxField "Dies sind Korrekturen" (Just False)
<*> fileAFormReq "Archiv"
<* submitButton
runDB $ do
case uploadResult of
FormMissing -> return ()
FormFailure _ -> addMessage "warning" "Bitte Eingabe korrigieren."
FormSuccess (isUpdate, fInfo) -> do
userId <- lift requireAuthId
let feed :: SubmissionId -> SubmissionContent -> StateT (Map SubmissionId (ResumableSink SubmissionContent (YesodDB UniWorX) SubmissionId)) (YesodDB UniWorX) ()
feed sId val = do
mSink <- gets $ Map.lookup sId
sink <- case mSink of
Just sink -> return sink
Nothing -> do
Submission{..} <- lift $ get404 sId
return . newResumableSink $ sinkSubmission submissionSheet userId (Just (sId, isUpdate))
sink' <- lift $ yield val ++$$ sink
case sink' of
Left _ -> error "sinkSubmission returned prematurely"
Right nSink -> modify $ Map.insert sId nSink
sinkSubmissions :: Sink SubmissionContent (YesodDB UniWorX) ()
sinkSubmissions = do
sinks <- execStateC Map.empty . awaitForever $ \case
v@(Right (sId, _)) -> lift $ feed sId v
(Left f@File{..}) -> case splitDirectories fileTitle of
(cID:rest)
| not (null rest) -> do
sId <- decrypt (CryptoID $ CI.mk cID :: CryptoFileNameSubmission)
lift . feed sId $ Left f{ fileTitle = joinPath rest }
| otherwise -> return ()
[] -> invalidArgs ["Encountered file/directory with empty name"]
lift $ mapM_ (void . closeResumableSink) sinks
runConduit $ fileSource fInfo =$= void consumeZip =$= extractRatings =$= void sinkSubmissions
(subTable, selectEncoding) <- generateFormPost . identifyForm "selection" . withFragment $ submissionTable
defaultLayout $(widgetFile "submission-list")
postSubmissionDownloadMultiArchiveR :: Handler TypedContent
postSubmissionDownloadMultiArchiveR = do
((selectResult, _), _) <- runFormPost . withFragment $ submissionTable
case selectResult of
FormMissing -> invalidArgs ["Missing submission numbers"]
FormFailure errs -> invalidArgs errs
FormSuccess ids -> do
(dbrunner, cleanup) <- getDBRunner
ratedSubmissions <- runDBRunner dbrunner $ do
submissions <- selectList [ SubmissionId <-. ids ] []
forM submissions $ \s@(Entity submissionId _) -> maybe (invalidArgs ["Invalid submission numbers"]) (return . (, s)) =<< getRating submissionId
(<* cleanup) . respondSource "application/zip" . transPipe (runDBRunner dbrunner) $ do
let
fileEntitySource' :: (Rating, Entity Submission) -> Source (YesodDB UniWorX) File
fileEntitySource' (rating, Entity submissionID Submission{..}) = do
cID <- encrypt submissionID
let
directoryName = CI.foldedCase $ ciphertext (cID :: CryptoFileNameSubmission)
fileEntitySource = do
submissionFileSource submissionID =$= Conduit.map entityVal
yieldM (ratingFile cID rating)
withinDirectory f@File{..} = f { fileTitle = directoryName </> fileTitle }
lastEditMb <- lift $ selectList [SubmissionEditSubmission ==. submissionID] [Desc SubmissionEditTime, LimitTo 1]
lastEditTime <- case lastEditMb of
[(submissionEditTime.entityVal -> time)] -> return time
_other -> liftIO getCurrentTime
yield $ File
{ fileModified = lastEditTime
, fileTitle = directoryName
, fileContent = Nothing
}
fileEntitySource =$= mapC withinDirectory
mapM_ fileEntitySource' ratedSubmissions =$= produceZip def =$= Conduit.map toFlushBuilder
getSubmissionDemoR, postSubmissionDemoR :: CryptoUUIDSubmission -> Handler Html
getSubmissionDemoR = postSubmissionDemoR
postSubmissionDemoR cID = do
submissionId <- decrypt cID
((uploadResult, uploadWidget), uploadEnctype) <- runFormPost . renderAForm FormStandard $ (,)
<$> areq checkBoxField "Dies ist eine Korrektur" (Just False)
<*> fileAFormReq "Datei"
<* submitButton
(submission, files) <- runDB $ do
submission <- do
submission@Submission{..} <- get404 submissionId
case uploadResult of
FormMissing -> return submission
FormFailure _ -> submission <$ addMessage "warning" "Bitte Eingabe korrigieren."
FormSuccess (isUpdate, fInfo) -> do
userId <- lift requireAuthId
let mimeType = defaultMimeLookup (fileName fInfo)
source
| mimeType == "application/zip" = fileSource fInfo =$= void consumeZip
| otherwise = do
let fileTitle = Text.unpack $ fileName fInfo
fileModified <- liftIO getCurrentTime
yieldM $ do
fileContent <- Just <$> runConduit (fileSource fInfo =$= foldC)
return File{..}
submissionId' <- runConduit $ source =$= extractRatings =$= sinkSubmission submissionSheet userId (Just (submissionId, isUpdate))
get404 submissionId'
files <- E.select . E.from $ \(sf `E.InnerJoin` f) -> E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
E.where_ (sf E.^. SubmissionFileSubmission E.==. E.val submissionId)
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate]
return (f, sf)
return (submission, files)
let
Rating'{..} = Rating'
{ ratingPoints = submissionRatingPoints submission
, ratingComment = submissionRatingComment submission
, ratingTime = submissionRatingTime submission
}
cID' <- encrypt submissionId
let
archiveBaseName = CI.foldedCase $ ciphertext (cID' :: CryptoFileNameSubmission)
archiveName = archiveBaseName <.> "zip"
defaultLayout $(widgetFile "submission")

View File

@ -7,6 +7,7 @@
, MultiParamTypeClasses , MultiParamTypeClasses
, TypeFamilies , TypeFamilies
, FlexibleContexts , FlexibleContexts
, PartialTypeSignatures
#-} #-}
module Handler.Term where module Handler.Term where
@ -18,7 +19,6 @@ import Handler.Utils
import Yesod.Form.Bootstrap3 import Yesod.Form.Bootstrap3
import Colonnade hiding (bool) import Colonnade hiding (bool)
import Yesod.Colonnade
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
@ -41,7 +41,7 @@ getTermShowR = do
selectRep $ do selectRep $ do
provideRep $ toJSON . map fst <$> runDB (E.select $ E.from termData) provideRep $ toJSON . map fst <$> runDB (E.select $ E.from termData)
provideRep $ do provideRep $ do
let colonnadeTerms = mconcat let colonnadeTerms = widgetColonnade $ mconcat
[ sortable Nothing "Kürzel" $ \(Entity tid Term{..},_) -> cell $ do [ sortable Nothing "Kürzel" $ \(Entity tid Term{..},_) -> cell $ do
-- Scrap this if to slow, create term edit page instead -- Scrap this if to slow, create term edit page instead
adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False adminLink <- handlerToWidget $ isAuthorized (TermEditExistR tid) False
@ -52,21 +52,21 @@ getTermShowR = do
$else $else
#{termToText termName} #{termToText termName}
|] |]
, sortable (Just "lecture-start") "Beginn Vorlesungen" $ \(Entity _ Term{..},_) -> , sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureStart stringCell $ formatTimeGerWD termLectureStart
, sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) -> , sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termLectureEnd stringCell $ formatTimeGerWD termLectureEnd
, sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) -> , sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) ->
textCell $ bool "" tickmark termActive textCell $ (bool "" tickmark termActive :: Text)
, sortable Nothing "Kursliste" $ anchorCell , sortable Nothing "Kursliste" $ anchorCell
(\(Entity tid _, _) -> TermCourseListR tid) (\(Entity tid _, _) -> TermCourseListR tid)
(\(_, E.Value numCourses) -> [whamlet|_{MsgNumCourses numCourses}|]) (\(_, E.Value numCourses) -> [whamlet|_{MsgNumCourses numCourses}|])
, sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) -> , sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termStart stringCell $ formatTimeGerWD termStart
, sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) -> , sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) ->
stringCell $ formatTimeGerWD termEnd stringCell $ formatTimeGerWD termEnd
, sortable Nothing "Feiertage im Semester" $ \(Entity _ Term{..},_) -> , sortable Nothing "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays stringCell $ (intercalate ", ") $ map formatTimeGerWD termHolidays
] ]
table <- dbTable def $ DBTable table <- dbTable def $ DBTable
{ dbtSQLQuery = termData { dbtSQLQuery = termData

View File

@ -40,15 +40,19 @@ import qualified Data.Conduit.List as C
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E import qualified Database.Esqueleto.Internal.Sql as E
import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
import Data.Map (Map)
import qualified Data.Map as Map
import Control.Monad.Writer.Class import Control.Monad.Writer.Class
------------------------------------------------ ------------------------------------------------
-- Unique Form Identifiers to avoid accidents -- -- Unique Form Identifiers to avoid accidents --
------------------------------------------------ ------------------------------------------------
data FormIdentifier = FIDcourse | FIDsheet | FIDsubmission | FIDsettings data FormIdentifier = FIDcourse | FIDsheet | FIDsubmission | FIDsettings | FIDcorrectors | FIDcorrectorTable | FIDcorrectionsUpload
deriving (Enum, Eq, Ord, Bounded, Read, Show) deriving (Enum, Eq, Ord, Bounded, Read, Show)
@ -530,3 +534,16 @@ mforced Field{..} FieldSettings{..} val = do
aforced :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) aforced :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> a -> AForm m a => Field m a -> FieldSettings site -> a -> AForm m a
aforced field settings val = formToAForm $ second pure <$> mforced field settings val aforced field settings val = formToAForm $ second pure <$> mforced field settings val
multiAction :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action)
=> Map action (MForm (HandlerT UniWorX IO) (FormResult a, Maybe Widget))
-> MForm (HandlerT UniWorX IO) (FormResult a, Widget)
multiAction acts = do
mr <- getMessageRender
let
options = OptionList [ Option (mr a) a (toPathPiece a) | a <- Map.keys acts ] fromPathPiece
(actionRes, actionView) <- mreq (selectField $ return options) "" Nothing
results <- sequence acts
let actionWidgets = Map.foldrWithKey (\act -> \case (_, Just w) -> ($(widgetFile "widgets/multiAction") :); (_, Nothing) -> id) [] results
actionResults = Map.map fst results
return ((actionResults Map.!) =<< actionRes, $(widgetFile "widgets/multiActionCollect"))

View File

@ -8,20 +8,26 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric, DeriveDataTypeable #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE PartialTypeSignatures #-}
module Handler.Utils.Submission module Handler.Utils.Submission
( SubmissionSinkException(..) ( AssignSubmissionException(..)
, assignSubmissions
, submissionFileSource, submissionFileQuery
, submissionMultiArchive
, SubmissionSinkException(..)
, sinkSubmission , sinkSubmission
) where ) where
import Import hiding ((.=)) import Import hiding ((.=), joinPath)
import Control.Lens import Control.Lens
import Control.Lens.Extras (is) import Control.Lens.Extras (is)
import Utils.Lens import Utils.Lens
import Control.Monad.State hiding (forM_) import Control.Monad.State hiding (forM_, mapM_,foldM)
import qualified Control.Monad.Random as Rand import qualified Control.Monad.Random as Rand
import Data.Maybe import Data.Maybe
@ -32,15 +38,21 @@ import qualified Data.Set as Set
import Data.Map (Map, (!?)) import Data.Map (Map, (!?))
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Data.Monoid (Monoid, Any(..)) import Data.Monoid (Monoid, Any(..))
import Generics.Deriving.Monoid (memptydefault, mappenddefault) import Generics.Deriving.Monoid (memptydefault, mappenddefault)
import Handler.Utils.Rating import Handler.Utils.Rating
import Handler.Utils.Zip
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.List as Conduit
import Data.Conduit.ResumableSink
import System.FilePath
data AssignSubmissionException = NoCorrectorsByProportion data AssignSubmissionException = NoCorrectorsByProportion
@ -49,12 +61,12 @@ data AssignSubmissionException = NoCorrectorsByProportion
instance Exception AssignSubmissionException instance Exception AssignSubmissionException
-- | Assigns all submissions according to sheet corrector loads -- | Assigns all submissions according to sheet corrector loads
assignSubmissions :: assignSubmissions :: SheetId -- ^ Sheet do distribute to correction
SheetId -- ^ Sheet do distribute to correction -> Maybe (Set SubmissionId) -- ^ Optionally restrict submission to consider
-> YesodDB UniWorX (Set SubmissionId -- ^ assigned submissions -> YesodDB UniWorX ( Set SubmissionId -- ^ assigned submissions
,Set SubmissionId -- ^ unassigend submissions (no tutors by load) , Set SubmissionId -- ^ unassigend submissions (no tutors by load)
) )
assignSubmissions sid = do assignSubmissions sid restriction = do
correctors <- selectList [SheetCorrectorSheet ==. sid] [] correctors <- selectList [SheetCorrectorSheet ==. sid] []
let corrsGroup = filter hasTutorialLoad correctors -- needed as List within Esqueleto let corrsGroup = filter hasTutorialLoad correctors -- needed as List within Esqueleto
let corrsProp = filter hasPositiveLoad correctors let corrsProp = filter hasPositiveLoad correctors
@ -74,7 +86,8 @@ assignSubmissions sid = do
E.where_ (tutorial E.^. TutorialTutor `E.in_` E.valList (map (sheetCorrectorUser . entityVal) corrsGroup)) E.where_ (tutorial E.^. TutorialTutor `E.in_` E.valList (map (sheetCorrectorUser . entityVal) corrsGroup))
return $ tutorial E.^. TutorialTutor return $ tutorial E.^. TutorialTutor
E.on $ user E.?. UserId `E.in_` E.justList tutors E.on $ user E.?. UserId `E.in_` E.justList tutors
E.where_ $ submission E.^. SubmissionSheet E.==. E.val sid E.where_ $ submission E.^. SubmissionSheet E.==. E.val sid
E.&&. maybe (E.val True) (submission E.^. SubmissionId `E.in_`) (E.valList . Set.toList <$> restriction)
E.orderBy [E.rand] -- randomize for fair tutor distribution E.orderBy [E.rand] -- randomize for fair tutor distribution
return (submission E.^. SubmissionId, user) -- , listToMaybe tutors) return (submission E.^. SubmissionId, user) -- , listToMaybe tutors)
@ -105,6 +118,57 @@ assignSubmissions sid = do
hasTutorialLoad = isJust . byTutorial . sheetCorrectorLoad . entityVal hasTutorialLoad = isJust . byTutorial . sheetCorrectorLoad . entityVal
submissionFileSource :: SubmissionId -> Source (YesodDB UniWorX) (Entity File)
submissionFileSource = E.selectSource . E.from . submissionFileQuery
submissionFileQuery :: SubmissionId -> E.SqlExpr (Entity SubmissionFile) `E.InnerJoin` E.SqlExpr (Entity File)
-> E.SqlQuery (E.SqlExpr (Entity File))
submissionFileQuery submissionID (sf `E.InnerJoin` f) = E.distinctOnOrderBy [E.asc $ f E.^. FileTitle] $ do
E.on (f E.^. FileId E.==. sf E.^. SubmissionFileFile)
E.where_ $ sf E.^. SubmissionFileSubmission E.==. E.val submissionID
E.where_ . E.not_ $ sf E.^. SubmissionFileIsDeletion -- TODO@gk: won't work as intended! Fix with refactor
E.orderBy [E.desc $ sf E.^. SubmissionFileIsUpdate] -- E.desc returns corrector updated data first
return f
submissionMultiArchive :: Set SubmissionId -> Handler TypedContent
submissionMultiArchive (Set.toList -> ids) = do
(dbrunner, cleanup) <- getDBRunner
ratedSubmissions <- runDBRunner dbrunner $ do
submissions <- selectList [ SubmissionId <-. ids ] []
forM submissions $ \s@(Entity submissionId _) -> maybe (invalidArgs ["Invalid submission numbers"]) (return . (, s)) =<< getRating submissionId
(<* cleanup) . respondSource "application/zip" . transPipe (runDBRunner dbrunner) $ do
let
fileEntitySource' :: (Rating, Entity Submission) -> Source (YesodDB UniWorX) File
fileEntitySource' (rating, Entity submissionID Submission{..}) = do
cID <- encrypt submissionID
let
directoryName = CI.foldedCase $ ciphertext (cID :: CryptoFileNameSubmission)
fileEntitySource = do
submissionFileSource submissionID =$= Conduit.map entityVal
yieldM (ratingFile cID rating)
withinDirectory f@File{..} = f { fileTitle = directoryName </> fileTitle }
lastEditMb <- lift $ selectList [SubmissionEditSubmission ==. submissionID] [Desc SubmissionEditTime, LimitTo 1]
lastEditTime <- case lastEditMb of
[(submissionEditTime.entityVal -> time)] -> return time
_other -> liftIO getCurrentTime
yield $ File
{ fileModified = lastEditTime
, fileTitle = directoryName
, fileContent = Nothing
}
fileEntitySource =$= mapC withinDirectory
mapM_ fileEntitySource' ratedSubmissions =$= produceZip def =$= Conduit.map toFlushBuilder
data SubmissionSinkState = SubmissionSinkState data SubmissionSinkState = SubmissionSinkState
@ -125,9 +189,9 @@ data SubmissionSinkException = DuplicateFileTitle FilePath
instance Exception SubmissionSinkException instance Exception SubmissionSinkException
sinkSubmission :: SheetId sinkSubmission :: UserId
-> UserId -> Either SheetId SubmissionId
-> Maybe (SubmissionId, Bool {-^ Is this a correction -}) -> Bool -- ^ Is this a correction
-> Sink SubmissionContent (YesodDB UniWorX) SubmissionId -> Sink SubmissionContent (YesodDB UniWorX) SubmissionId
-- ^ Replace the currently saved files for the given submission (either -- ^ Replace the currently saved files for the given submission (either
-- corrected files or original ones, depending on arguments) with the supplied -- corrected files or original ones, depending on arguments) with the supplied
@ -137,25 +201,28 @@ sinkSubmission :: SheetId
-- are deleted (or marked as deleted in the case of this being a correction). -- are deleted (or marked as deleted in the case of this being a correction).
-- --
-- A 'Submission' is created if no 'SubmissionId' is supplied -- A 'Submission' is created if no 'SubmissionId' is supplied
sinkSubmission sheetId userId mExists = do sinkSubmission userId mExists isUpdate = do
now <- liftIO getCurrentTime sId <- lift $ case mExists of
let Left sheetId -> do
submissionSheet = sheetId let
submissionRatingPoints = Nothing submissionSheet = sheetId
submissionRatingComment = Nothing submissionRatingPoints = Nothing
submissionRatingBy = Nothing submissionRatingComment = Nothing
submissionRatingTime = Nothing submissionRatingBy = Nothing
submissionRatingTime = Nothing
(sId, isUpdate) <- lift $ maybe ((, False) <$> (insert Submission{..} >>= (\sid -> sid <$ insert (SubmissionEdit userId now sid)))) return mExists sId <- insert Submission{..}
-- now <- liftIO getCurrentTime
-- insert $ SubmissionEdit userId now sId -- This is done automatically during 'sinkSubmission'' iff the given submission is nonempty
return sId
Right sId -> return sId
sId <$ sinkSubmission' sId isUpdate sId <$ sinkSubmission' sId isUpdate
where where
tell = modify . mappend tell = modify . mappend
sinkSubmission' :: SubmissionId sinkSubmission' :: SubmissionId
-> Bool -- ^ Is this a correction -> Bool -- ^ Is this a correction
-> Sink SubmissionContent (YesodDB UniWorX) () -> Sink SubmissionContent (YesodDB UniWorX) ()
sinkSubmission' submissionId isUpdate = lift . finalize <=< execStateLC mempty . Conduit.mapM_ $ \case sinkSubmission' submissionId isUpdate = lift . finalize <=< execStateLC mempty . Conduit.mapM_ $ \case
Left file@(File{..}) -> do Left file@(File{..}) -> do
$logDebugS "sinkSubmission" . tshow $ (submissionId, fileTitle) $logDebugS "sinkSubmission" . tshow $ (submissionId, fileTitle)
@ -303,3 +370,43 @@ sinkSubmission sheetId userId mExists = do
, SubmissionRatingBy =. Nothing , SubmissionRatingBy =. Nothing
, SubmissionRatingComment =. Nothing , SubmissionRatingComment =. Nothing
] ]
sinkMultiSubmission :: UserId
-> Bool {-^ Are these corrections -}
-> Sink SubmissionContent (YesodDB UniWorX) (Set SubmissionId)
-- ^ Expects all supplied 'SubmissionContent' to contain an encrypted 'SubmissionId' and replaces the currently saved files for the respective submissions (either corrected files or original ones, depending on arguments) with the supplied 'SubmissionContent'.
--
-- Files that don't occur in the 'SubmissionContent' but are in the database are deleted (or marked as deleted in the case of this being a correction).
sinkMultiSubmission userId isUpdate = do
let
feed :: SubmissionId
-> SubmissionContent
-> StateT
(Map SubmissionId (ResumableSink SubmissionContent (YesodDB UniWorX) SubmissionId))
(YesodDB UniWorX)
()
feed sId val = do
mSink <- gets $ Map.lookup sId
sink <- case mSink of
Just sink -> return sink
Nothing -> do
-- Submission{..} <- lift $ get404 sId
return . newResumableSink $ sinkSubmission userId (Right sId) isUpdate
sink' <- lift $ yield val ++$$ sink
case sink' of
Left _ -> error "sinkSubmission returned prematurely"
Right nSink -> modify $ Map.insert sId nSink
sinks <- execStateLC Map.empty . awaitForever $ \case
v@(Right (sId, _)) -> lift $ feed sId v
(Left f@File{..}) -> do
let
tryDecrypt :: FilePath -> _ (Either CryptoIDError SubmissionId)
tryDecrypt (CI.mk -> ciphertext) = try $ decrypt (CryptoID{..} :: CryptoFileNameSubmission)
acc (Just cID, fp) segment = return (Just cID, fp ++ [segment])
acc (Nothing , fp) segment = do
msId <- tryDecrypt segment
return . either (const id) (set _1 . Just) msId $ (Nothing, fp)
(msId, (joinPath -> fileTitle')) <- foldM acc (Nothing, []) $ splitDirectories fileTitle
lift . maybe (const $ return ()) feed msId $ Left f{ fileTitle = fileTitle' }
fmap Map.keysSet . lift $ mapM (void . closeResumableSink) sinks

View File

@ -35,9 +35,6 @@ numberColonnade = headed "Nr" (fromString.show)
pairColonnade :: (Functor h) => Colonnade h a c -> Colonnade h b c -> Colonnade h (a,b) c 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] pairColonnade a b = mconcat [ lmap fst a, lmap snd b]
i18nCell :: RenderMessage site a => a -> Cell site
i18nCell msg = cell [whamlet|_{msg}|]
-- Table Modification -- Table Modification
encodeHeadedWidgetTableNumbered :: Attribute -> Colonnade Headed a (WidgetT site IO ()) -> [a] -> WidgetT site IO () encodeHeadedWidgetTableNumbered :: Attribute -> Colonnade Headed a (WidgetT site IO ()) -> [a] -> WidgetT site IO ()
@ -94,3 +91,5 @@ headedRowSelector toExternal fromExternal attrs colonnade tdata = do
return ( catMaybes <$> collectResult selectionResults return ( catMaybes <$> collectResult selectionResults
, encodeCellTable attrs (pairColonnade selColonnade colonnade) (zip [0..] tdata) , encodeCellTable attrs (pairColonnade selColonnade colonnade) (zip [0..] tdata)
) )

View File

@ -1,6 +1,7 @@
{-# LANGUAGE NoImplicitPrelude {-# LANGUAGE NoImplicitPrelude
, ExistentialQuantification , ExistentialQuantification
, RecordWildCards , RecordWildCards
, NamedFieldPuns
, OverloadedStrings , OverloadedStrings
, TemplateHaskell , TemplateHaskell
, QuasiQuotes , QuasiQuotes
@ -10,29 +11,37 @@
, FlexibleInstances , FlexibleInstances
, MultiParamTypeClasses , MultiParamTypeClasses
, TypeFamilies , TypeFamilies
, ScopedTypeVariables
, TupleSections
, RankNTypes
#-} #-}
module Handler.Utils.Table.Pagination module Handler.Utils.Table.Pagination
( SortColumn(..), SortDirection(..) ( SortColumn(..), SortDirection(..)
, FilterColumn(..), IsFilterColumn , FilterColumn(..), IsFilterColumn
, DBRow(..), DBOutput , DBRow(..), DBOutput
, DBTable(..) , DBTable(..), IsDBTable(..)
, PaginationSettings(..) , PaginationSettings(..)
, PSValidator(..) , PSValidator(..)
, Sortable(..), sortable , defaultFilter, defaultSorting
, restrictFilter, restrictSorting
, ToSortable(..), Sortable(..), sortable
, dbTable , dbTable
, widgetColonnade, formColonnade
, textCell, stringCell, i18nCell, anchorCell
, formCell, DBFormResult, getDBFormResult
, dbRow, dbSelect
) where ) where
import Handler.Utils.Table.Pagination.Types import Handler.Utils.Table.Pagination.Types
import Import import Import hiding (Proxy(..))
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect,unsafeSqlValue) import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect,unsafeSqlValue)
import qualified Database.Esqueleto.Internal.Language as E (From) import qualified Database.Esqueleto.Internal.Language as E (From)
import Text.Blaze (Attribute) import Text.Blaze (Attribute)
import qualified Text.Blaze.Html5.Attributes as Html5 import qualified Text.Blaze.Html5.Attributes as Html5
import qualified Text.Blaze.Html5 as Html5 import qualified Text.Blaze.Html5 as Html5
import Yesod.Core.Types (Body(..),GWData(..),WidgetT(..))
import qualified Data.Binary.Builder as Builder import qualified Data.Binary.Builder as Builder
@ -42,6 +51,7 @@ import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Control.Monad.RWS hiding ((<>), Foldable(..), mapM_, forM_) import Control.Monad.RWS hiding ((<>), Foldable(..), mapM_, forM_)
import Control.Monad.Writer hiding ((<>), Foldable(..), mapM_, forM_)
import Data.Map (Map, (!)) import Data.Map (Map, (!))
import qualified Data.Map as Map import qualified Data.Map as Map
@ -49,13 +59,17 @@ import qualified Data.Map as Map
import Data.Profunctor (lmap) import Data.Profunctor (lmap)
import Colonnade hiding (bool, fromMaybe, singleton) import Colonnade hiding (bool, fromMaybe, singleton)
import qualified Colonnade (singleton)
import Colonnade.Encode import Colonnade.Encode
import Yesod.Colonnade
import Text.Hamlet (hamletFile) import Text.Hamlet (hamletFile)
import Data.Ratio ((%)) import Data.Ratio ((%))
import Control.Lens
import Data.Proxy
data SortColumn t = forall a. PersistField a => SortColumn { getSortColumn :: t -> E.SqlExpr (E.Value a) } data SortColumn t = forall a. PersistField a => SortColumn { getSortColumn :: t -> E.SqlExpr (E.Value a) }
@ -99,38 +113,38 @@ instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterColumn t cont, Mon
data DBRow r = DBRow data DBRow r = DBRow
{ dbrIndex, dbrCount :: Int64 { dbrOutput :: r
, dbrOutput :: r , dbrIndex, dbrCount :: Int64
} } deriving (Show, Read, Eq, Ord)
class DBOutput r r' where class DBOutput r r' where
dbProj :: r -> r' dbProj :: r -> r'
instance DBOutput r r where instance DBOutput (DBRow r) (DBRow r) where
dbProj = id dbProj = id
instance DBOutput (DBRow r) r where instance DBOutput (DBRow r) r where
dbProj = dbrOutput dbProj = dbrOutput
instance DBOutput (DBRow r) (Int64, r) where instance DBOutput (DBRow r) (Int64, r) where
dbProj = (,) <$> dbrIndex <*> dbrOutput dbProj = (,) <$> dbrIndex <*> dbrOutput
data DBTable m x = forall a r r' h i t.
data DBTable = forall a r r' h i t.
( ToSortable h, Functor h ( ToSortable h, Functor h
, E.SqlSelect a r, DBOutput (DBRow r) r' , E.SqlSelect a r, DBOutput (DBRow r) r'
, PathPiece i , PathPiece i
, E.From E.SqlQuery E.SqlExpr E.SqlBackend t , E.From E.SqlQuery E.SqlExpr E.SqlBackend t
) => DBTable ) => DBTable
{ dbtSQLQuery :: t -> E.SqlQuery a { dbtSQLQuery :: t -> E.SqlQuery a
, dbtColonnade :: Colonnade h r' (Cell UniWorX) , dbtColonnade :: Colonnade h r' (DBCell m x)
, dbtSorting :: Map Text (SortColumn t) , dbtSorting :: Map (CI Text) (SortColumn t)
, dbtFilter :: Map Text (FilterColumn t) , dbtFilter :: Map (CI Text) (FilterColumn t)
, dbtAttrs :: Attribute , dbtAttrs :: Attribute -- FIXME: currently unused
, dbtIdent :: i , dbtIdent :: i
} }
data PaginationSettings = PaginationSettings data PaginationSettings = PaginationSettings
{ psSorting :: [(Text, SortDirection)] { psSorting :: [(CI Text, SortDirection)]
, psFilter :: Map Text [Text] , psFilter :: Map (CI Text) [Text]
, psLimit :: Int64 , psLimit :: Int64
, psPage :: Int64 , psPage :: Int64
, psShortcircuit :: Bool , psShortcircuit :: Bool
@ -145,9 +159,9 @@ instance Default PaginationSettings where
, psShortcircuit = False , psShortcircuit = False
} }
newtype PSValidator = PSValidator { runPSValidator :: DBTable -> Maybe PaginationSettings -> ([SomeMessage UniWorX], PaginationSettings) } newtype PSValidator m x = PSValidator { runPSValidator :: DBTable m x -> Maybe PaginationSettings -> ([SomeMessage UniWorX], PaginationSettings) }
instance Default PSValidator where instance Default (PSValidator m x) where
def = PSValidator $ \DBTable{..} -> \case def = PSValidator $ \DBTable{..} -> \case
Nothing -> def Nothing -> def
Just ps -> swap . (\act -> execRWS act () ps) $ do Just ps -> swap . (\act -> execRWS act () ps) $ do
@ -156,15 +170,94 @@ instance Default PSValidator where
modify $ \ps -> ps { psLimit = psLimit def } modify $ \ps -> ps { psLimit = psLimit def }
tell . pure $ SomeMessage MsgPSLimitNonPositive tell . pure $ SomeMessage MsgPSLimitNonPositive
defaultFilter :: Map (CI Text) [Text] -> PSValidator m x -> PSValidator m x
defaultFilter psFilter (runPSValidator -> f) = PSValidator g
where
g dbTable Nothing = over _2 (\s -> s { psFilter }) $ f dbTable Nothing
g dbTable x = f dbTable x
dbTable :: PSValidator -> DBTable -> Handler Widget defaultSorting :: [(CI Text, SortDirection)] -> PSValidator m x -> PSValidator m x
defaultSorting psSorting (runPSValidator -> f) = PSValidator g
where
g dbTable Nothing = over _2 (\s -> s { psSorting }) $ f dbTable Nothing
g dbTable x = f dbTable x
restrictFilter :: (CI Text -> [Text] -> Bool) -> PSValidator m x -> PSValidator m x
restrictFilter restrict (runPSValidator -> f) = PSValidator $ \dbTable ps -> over _2 restrict' $ f dbTable ps
where
restrict' p = p { psFilter = Map.filterWithKey restrict $ psFilter p }
restrictSorting :: (CI Text -> SortDirection -> Bool) -> PSValidator m x -> PSValidator m x
restrictSorting restrict (runPSValidator -> f) = PSValidator $ \dbTable ps -> over _2 restrict' $ f dbTable ps
where
restrict' p = p { psSorting = filter (uncurry restrict) $ psSorting p }
class (MonadHandler m, Monoid x) => IsDBTable (m :: * -> *) (x :: *) where
type DBResult m x :: *
-- type DBResult' m x :: *
data DBCell m x :: *
cellAttrs :: Lens' (DBCell m x) [(Text, Text)]
cellContents :: DBCell m x -> WriterT x m Widget
cell :: Widget -> DBCell m x
-- dbWidget :: Proxy m -> Proxy x -> Iso' (DBResult m x) (Widget, DBResult' m x)
dbWidget :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => Proxy m -> Proxy x -> DBResult m x -> m' Widget
runDBTable :: forall m'. (MonadHandler m', HandlerSite m' ~ UniWorX) => m (x, Widget) -> m' (DBResult m x)
instance IsDBTable (WidgetT UniWorX IO) () where
type DBResult (WidgetT UniWorX IO) () = Widget
-- type DBResult' (WidgetT UniWorX IO) () = ()
data DBCell (WidgetT UniWorX IO) () = WidgetCell
{ dbCellAttrs :: [(Text, Text)]
, dbCellContents :: Widget
}
cellAttrs = lens dbCellAttrs $ \w as -> w { dbCellAttrs = as }
cellContents = return . dbCellContents
cell = WidgetCell []
-- dbWidget Proxy Proxy = iso (, ()) $ view _1
dbWidget Proxy Proxy = return
runDBTable = return . join . fmap (view _2)
instance Monoid a => IsDBTable (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) where
-- type DBResult (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) = ((FormResult a, Widget), Enctype)
type DBResult (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) = Form a
-- type DBResult' (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) = (FormResult a, Enctype)
data DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a) = FormCell
{ formCellAttrs :: [(Text, Text)]
, formCellContents :: MForm (HandlerT UniWorX IO) (FormResult a, Widget)
}
cellAttrs = lens formCellAttrs $ \w as -> w { formCellAttrs = as }
cellContents = WriterT . fmap swap . formCellContents
cell widget = FormCell [] $ return (mempty, widget)
-- dbWidget Proxy Proxy = iso ((,) <$> view (_1._2) <*> ((,) <$> view (_1._1) <*> view _2))
-- ((,) <$> ((,) <$> view (_2._1) <*> view _1) <*> view (_2._2))
dbWidget Proxy Proxy = liftHandlerT . fmap (view $ _1 . _2) . runFormPost
-- runDBTable :: MForm (HandlerT UniWorX IO) (FormResult a, Widget) -> m ((FormResult a, Widget), Enctype)
-- runDBTable form = liftHandlerT . runFormPost $ \html -> over _2 (<> toWidget html) <$> form
-- runDBTable :: MForm (HandlerT UniWorX IO) (FormResult a, Widget) -> m (Html -> MForm (HandleT UniWorX IO) (FormResult a, Widget))
runDBTable = return . withFragment
instance IsDBTable m a => IsString (DBCell m a) where
fromString = cell . fromString
dbTable :: forall m x. IsDBTable m x => PSValidator m x -> DBTable m x -> Handler (DBResult m x)
dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent), dbtColonnade = (lmap dbProj -> dbtColonnade), .. }) = do dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent), dbtColonnade = (lmap dbProj -> dbtColonnade), .. }) = do
let let
sortingOptions = mkOptionList sortingOptions = mkOptionList
[ Option t' (t, d) t' [ Option t' (t, d) t'
| (t, _) <- mapToList dbtSorting | (t, _) <- mapToList dbtSorting
, d <- [SortAsc, SortDesc] , d <- [SortAsc, SortDesc]
, let t' = t <> "-" <> toPathPiece d , let t' = CI.foldedCase t <> "-" <> toPathPiece d
] ]
(_, defPS) = runPSValidator dbtable Nothing (_, defPS) = runPSValidator dbtable Nothing
wIdent n wIdent n
@ -181,7 +274,7 @@ dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent),
psResult <- runInputGetResult $ PaginationSettings psResult <- runInputGetResult $ PaginationSettings
<$> (fromMaybe [] <$> iopt (multiSelectField $ return sortingOptions) (wIdent "sorting")) <$> (fromMaybe [] <$> iopt (multiSelectField $ return sortingOptions) (wIdent "sorting"))
<*> (Map.mapMaybe ((\args -> args <$ guard (not $ null args)) =<<) <$> Map.traverseWithKey (\k _ -> iopt multiTextField $ wIdent k) dbtFilter) <*> (Map.mapMaybe ((\args -> args <$ guard (not $ null args)) =<<) <$> Map.traverseWithKey (\k _ -> iopt multiTextField . wIdent $ CI.foldedCase k) dbtFilter)
<*> (fromMaybe (psLimit defPS) <$> iopt intField (wIdent "pagesize")) <*> (fromMaybe (psLimit defPS) <$> iopt intField (wIdent "pagesize"))
<*> (fromMaybe (psPage defPS) <$> iopt intField (wIdent "page")) <*> (fromMaybe (psPage defPS) <$> iopt intField (wIdent "page"))
<*> ireq checkBoxField (wIdent "table-only") <*> ireq checkBoxField (wIdent "table-only")
@ -212,25 +305,44 @@ dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent),
rowCount rowCount
| ((_, E.Value n), _):_ <- rows' = n | ((_, E.Value n), _):_ <- rows' = n
| otherwise = 0 | otherwise = 0
rows = map (\((E.Value i, E.Value n), r) -> DBRow i n r) rows' rows = map (\((E.Value dbrIndex, E.Value dbrCount), dbrOutput) -> DBRow{..}) rows'
bool return (sendResponse <=< tblLayout) psShortcircuit $ do table' :: WriterT x m Widget
getParams <- handlerToWidget $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest table' = do
let table = $(widgetFile "table/colonnade") getParams <- liftHandlerT $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
pageCount = max 1 . ceiling $ rowCount % psLimit
pageNumbers = [0..pred pageCount] let
tblLink f = decodeUtf8 . Builder.toLazyByteString . renderQueryText True $ f getParams tblLink f = decodeUtf8 . Builder.toLazyByteString . renderQueryText True $ f getParams
withSortLinks Sortable{ sortableContent = Cell{..}, .. } = Cell genHeaders SortableP{..} = forM (toSortable . oneColonnadeHead <$> getColonnade dbtColonnade) $ \Sortable{..} -> do
{ cellContents = $(widgetFile "table/sortable-header") widget <- cellContents sortableContent
, cellAttrs = maybe mempty (const sortableAttr) sortableKey <> cellAttrs let
}
where
directions = [dir | (k, dir) <- psSorting, Just k == sortableKey ] directions = [dir | (k, dir) <- psSorting, Just k == sortableKey ]
sortableAttr = Html5.class_ . fromString . unwords $ "sortable" : foldMap toAttr directions isSortable = isJust sortableKey
toAttr SortAsc = ["sorted-asc"] isSorted = (`elem` directions)
toAttr SortDesc = ["sorted-desc"] attrs = sortableContent ^. cellAttrs
$(widgetFile "table/layout") return $(widgetFile "table/cell/header")
columnCount :: Int64
columnCount = olength64 $ getColonnade dbtColonnade
wHeaders <- maybe (return Nothing) (fmap Just . genHeaders) pSortable
wRows <- forM rows $ \row -> forM (oneColonnadeEncode <$> getColonnade dbtColonnade) $ \(($ row) -> cell) -> do
widget <- cellContents cell
let attrs = cell ^. cellAttrs
return $(widgetFile "table/cell/body")
let table = $(widgetFile "table/colonnade")
pageCount = max 1 . ceiling $ rowCount % psLimit
pageNumbers = [0..pred pageCount]
return $(widgetFile "table/layout")
dbWidget' :: DBResult m x -> Handler Widget
dbWidget' = dbWidget (Proxy :: Proxy m) (Proxy :: Proxy x)
bool return (sendResponse <=< tblLayout <=< dbWidget') psShortcircuit <=< runDBTable . fmap swap $ runWriterT table'
where where
tblLayout :: Widget -> Handler Html tblLayout :: Widget -> Handler Html
tblLayout tbl' = do tblLayout tbl' = do
@ -240,22 +352,62 @@ dbTable PSValidator{..} dbtable@(DBTable{ dbtIdent = (toPathPiece -> dbtIdent),
setParam :: Text -> Maybe Text -> QueryText -> QueryText setParam :: Text -> Maybe Text -> QueryText -> QueryText
setParam key v qt = (key, v) : [ i | i@(key', _) <- qt, key' /= key ] setParam key v qt = (key, v) : [ i | i@(key', _) <- qt, key' /= key ]
widgetFromCell :: --- DBCell utility functions
(Attribute -> WidgetT site IO () -> WidgetT site IO ())
-> Cell site
-> WidgetT site IO ()
widgetFromCell f (Cell attrs contents) =
f attrs contents
td,th ::
Attribute -> WidgetT site IO () -> WidgetT site IO ()
td = liftParent Html5.td widgetColonnade :: Headedness h
th = liftParent Html5.th => Colonnade h r (DBCell (WidgetT UniWorX IO) ())
-> Colonnade h r (DBCell (WidgetT UniWorX IO) ())
widgetColonnade = id
liftParent :: (Html -> Html) -> Attribute -> WidgetT site IO a -> WidgetT site IO a formColonnade :: (Headedness h, Monoid a)
liftParent el attrs (WidgetT f) = WidgetT $ \hdata -> do => Colonnade h r (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a))
(a,gwd) <- f hdata -> Colonnade h r (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO)) (FormResult a))
let Body bodyFunc = gwdBody gwd formColonnade = id
newBodyFunc render =
el Html5.! attrs $ (bodyFunc render) textCell, stringCell, i18nCell :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a
return (a,gwd { gwdBody = Body newBodyFunc }) stringCell = textCell
i18nCell = textCell
textCell msg = cell [whamlet|_{msg}|]
anchorCell :: IsDBTable m a
=> (r -> Route UniWorX)
-> (r -> Widget)
-> (r -> DBCell m a)
anchorCell mkRoute mkWidget val = cell $(widgetFile "table/cell/link")
where
route = mkRoute val
widget = mkWidget val
newtype DBFormResult r i a = DBFormResult (Map i (r, a -> a))
instance Ord i => Monoid (DBFormResult r i a) where
mempty = DBFormResult Map.empty
(DBFormResult m1) `mappend` (DBFormResult m2) = DBFormResult $ Map.unionWith (\(r, f1) (_, f2) -> (r, f2 . f1)) m1 m2
getDBFormResult :: forall r i a. Ord i => (r -> a) -> DBFormResult r i a -> Map i a
getDBFormResult initial (DBFormResult m) = Map.map (\(r, f) -> f $ initial r) m
formCell :: forall r i a. Ord i
=> (r -> MForm (HandlerT UniWorX IO) i)
-> (r -> i -> MForm (HandlerT UniWorX IO) (FormResult (a -> a), Widget))
-> (r -> DBCell ((RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO))) (FormResult (DBFormResult r i a)))
formCell genIndex genForm input = FormCell
{ formCellAttrs = []
, formCellContents = do -- MForm (HandlerT UniWorX IO) (FormResult (Map i (Endo a)), Widget)
i <- genIndex input
(edit, w) <- genForm input i
return (DBFormResult . Map.singleton i . (input,) <$> edit, w)
}
-- Predefined colonnades
dbRow :: forall h r m a. (Headedness h, IsDBTable m a) => Colonnade h (DBRow r) (DBCell m a)
dbRow = Colonnade.singleton (headednessPure $ textCell MsgNrColumn) $ \DBRow{ dbrIndex } -> textCell $ tshow dbrIndex
dbSelect :: forall h r i a. (Headedness h, Ord i, PathPiece i)
=> Setter' a Bool
-> (r -> MForm (HandlerT UniWorX IO) i)
-> Colonnade h r (DBCell ((RWST (Maybe (Env, FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerT UniWorX IO))) (FormResult (DBFormResult r i a)))
dbSelect resLens genIndex = Colonnade.singleton (headednessPure $ textCell MsgSelectColumn) $ \r -> flip (formCell genIndex) r $ \_ i -> do
(selResult, selWidget) <- mreq checkBoxField ("" { fsName = Just $ "select-" <> toPathPiece i }) (Just False)
return (set resLens <$> selResult, [whamlet|^{fvInput selWidget}|])

View File

@ -11,12 +11,14 @@ import Import hiding (singleton)
import Colonnade import Colonnade
import Colonnade.Encode import Colonnade.Encode
import Data.CaseInsensitive (CI)
data Sortable a = Sortable data Sortable a = Sortable
{ sortableKey :: Maybe Text { sortableKey :: Maybe (CI Text)
, sortableContent :: a , sortableContent :: a
} }
sortable :: Maybe Text -> c -> (a -> c) -> Colonnade Sortable a c sortable :: Maybe (CI Text) -> c -> (a -> c) -> Colonnade Sortable a c
sortable k h = singleton (Sortable k h) sortable k h = singleton (Sortable k h)
instance Headedness Sortable where instance Headedness Sortable where
@ -40,4 +42,3 @@ instance ToSortable Headed where
instance ToSortable Headless where instance ToSortable Headless where
pSortable = Nothing pSortable = Nothing

View File

@ -109,7 +109,7 @@ data Load -- = ByTutorial { countsToLoad :: Bool } | ByProportion { load :: Rati
= Load { byTutorial :: Maybe Bool -- ^ Just all from Tutorial, True if counting towards overall workload = Load { byTutorial :: Maybe Bool -- ^ Just all from Tutorial, True if counting towards overall workload
, byProportion :: Rational -- ^ workload proportion of all submission not assigned to tutorial leaders , byProportion :: Rational -- ^ workload proportion of all submission not assigned to tutorial leaders
} }
deriving (Show, Read, Eq) deriving (Show, Read, Eq, Ord)
derivePersistField "Load" derivePersistField "Load"
instance Semigroup Load where instance Semigroup Load where
@ -169,14 +169,30 @@ termFromText t
, Right season <- seasonFromChar s , Right season <- seasonFromChar s
= Right TermIdentifier{..} = Right TermIdentifier{..}
| otherwise = Left $ "Invalid TermIdentifier: “" <> t <> "" | otherwise = Left $ "Invalid TermIdentifier: “" <> t <> ""
termToRational :: TermIdentifier -> Rational
termToRational TermIdentifier{..} = fromInteger year + seasonOffset
where
seasonOffset
| Summer <- season = 0
| Winter <- season = 0.5
termFromRational :: Rational -> TermIdentifier
termFromRational n = TermIdentifier{..}
where
year = floor n
remainder = n - (fromInteger $ floor n)
season
| remainder == 0 = Summer
| otherwise = Winter
instance PersistField TermIdentifier where instance PersistField TermIdentifier where
toPersistValue = PersistText . termToText toPersistValue = PersistRational . termToRational
fromPersistValue (PersistText t) = termFromText t fromPersistValue (PersistRational t) = Right $ termFromRational t
fromPersistValue x = Left $ "Expected TermIdentifier, received: " <> tshow x fromPersistValue x = Left $ "Expected TermIdentifier, received: " <> tshow x
instance PersistFieldSql TermIdentifier where instance PersistFieldSql TermIdentifier where
sqlType _ = SqlString sqlType _ = SqlNumeric 5 1
instance ToHttpApiData TermIdentifier where instance ToHttpApiData TermIdentifier where
toUrlPiece = termToText toUrlPiece = termToText
@ -193,20 +209,20 @@ instance ToJSON TermIdentifier where
instance FromJSON TermIdentifier where instance FromJSON TermIdentifier where
parseJSON = withText "Term" $ either (fail . Text.unpack) return . termFromText parseJSON = withText "Term" $ either (fail . Text.unpack) return . termFromText
{- Must be defined in a later module: {- Must be defined in a later module:
termField :: Field (HandlerT UniWorX IO) TermIdentifier termField :: Field (HandlerT UniWorX IO) TermIdentifier
termField = checkMMap (return . termFromText) termToText textField termField = checkMMap (return . termFromText) termToText textField
-- TODO: this is too simple and inconvenient, use selector and year picker -- TODO: this is too simple and inconvenient, use selector and year picker
-} -}
withinTerm :: Day -> TermIdentifier -> Bool withinTerm :: Day -> TermIdentifier -> Bool
time `withinTerm` term = timeYear `mod` 100 == termYear `mod` 100 time `withinTerm` term = timeYear `mod` 100 == termYear `mod` 100
where where
timeYear = fst3 $ toGregorian time timeYear = fst3 $ toGregorian time
termYear = year term termYear = year term
data StudyFieldType = FieldPrimary | FieldSecondary data StudyFieldType = FieldPrimary | FieldSecondary
deriving (Eq, Ord, Enum, Show, Read, Bounded) deriving (Eq, Ord, Enum, Show, Read, Bounded)
@ -216,9 +232,10 @@ derivePersistField "StudyFieldType"
-- Skins / Themes -- Skins / Themes
data Theme --Simply add Themes to this type only. CamelCase will be converted to "-lower" data Theme --Simply add Themes to this type only. CamelCase will be converted to "-lower"
= Default = Default
| Lavender
| NeutralBlue | NeutralBlue
| AberdeenReds | AberdeenReds
| MintGreen | MossGreen
| SkyLove | SkyLove
deriving (Eq,Ord,Bounded,Enum) deriving (Eq,Ord,Bounded,Enum)
@ -242,5 +259,3 @@ instance Default Theme where
-} -}
derivePersistField "Theme" derivePersistField "Theme"

View File

@ -43,6 +43,10 @@ getMsgRenderer = do
mr <- getMessageRender mr <- getMessageRender
return $ MsgRenderer (mr . SomeMessage :: forall msg. RenderMessage site msg => msg -> Text) return $ MsgRenderer (mr . SomeMessage :: forall msg. RenderMessage site msg => msg -> Text)
instance Monad FormResult where
FormMissing >>= _ = FormMissing
(FormFailure errs) >>= _ = FormFailure errs
(FormSuccess a) >>= f = f a
--------------------- ---------------------
-- Text and String -- -- Text and String --

View File

@ -16,7 +16,7 @@ import Language.Haskell.TH
-- Tuples -- -- Tuples --
------------ ------------
-- Alternatively uses lenses: "^. _3" projects the 3rd component of an n-tuple for any n >=3, requires import Control.Lens
projNI :: Int -> Int -> ExpQ -- generic projection gives I-th element of N-tuple, i.e. snd3 = $(projNI 3 2) --ghci -fth projNI :: Int -> Int -> ExpQ -- generic projection gives I-th element of N-tuple, i.e. snd3 = $(projNI 3 2) --ghci -fth
-- $(projN n m) :: (t1,..,tn) -> tm (for m<=n) -- $(projN n m) :: (t1,..,tn) -> tm (for m<=n)
projNI n i = lamE [pat] rhs projNI n i = lamE [pat] rhs

View File

@ -36,10 +36,7 @@
<a href=@{TermEditR}>Neues Semester anlegen <a href=@{TermEditR}>Neues Semester anlegen
<li .list-group-item> <li .list-group-item>
<a href=@{CourseNewR}>Kurse anlegen <a href=@{CourseNewR}>Kurse anlegen
<li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
<hr> <hr>
<div .container> <div .container>

View File

@ -0,0 +1,5 @@
<div .container>
<form method=POST enctype=#{tableEncoding}>
^{table}
<button type=submit>
_{MsgBtnSubmit}

View File

@ -1,4 +1,4 @@
0<div .container> <div .container>
<div .scrolltable> <div .scrolltable>
<table .table.table--striped.table--hover.table--vertical> <table .table.table--striped.table--hover.table--vertical>
$maybe school <- schoolMB $maybe school <- schoolMB
@ -22,18 +22,17 @@
#{participants} #{participants}
$maybe capacity <- courseCapacity course $maybe capacity <- courseCapacity course
\ von #{capacity} \ von #{capacity}
<tr .table__row> $maybe regFrom <- courseRegisterFrom course
<th #registration>Anmeldezeitraum <tr .table__row>
<td> <th #registration>Anmeldezeitraum
$maybe regFrom <- courseRegisterFrom course <td>
#{formatTimeGerWD regFrom} Ab #{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course $maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo} \ bis #{formatTimeGerWD regTo}
$if registrationOpen
<tr .table__row> <tr .table__row>
<th> <th>
<td> <td>
$if registrationOpen
<div .course__registration.container> <div .course__registration.container>
<form method=post action=@{CourseR tid csh CRegisterR} enctype=#{regEnctype}> <form method=post action=@{CourseR tid csh CRegisterR} enctype=#{regEnctype}>
$# regWidget is defined through templates/widgets/registerForm $# regWidget is defined through templates/widgets/registerForm

View File

@ -16,7 +16,7 @@
--font-logo: "Roboto", var(--font-base); --font-logo: "Roboto", var(--font-base);
/* DIMENSIONS */ /* DIMENSIONS */
--header-height: 80px; --header-height: 100px;
--header-height-collapsed: 50px; --header-height-collapsed: 50px;
} }
@ -38,15 +38,25 @@ body {
/* THEMES */ /* THEMES */
body { body {
/* DEFAULT THEME */ /* DEFAULT LMU THEME */
--color-primary: #4C7A9C; --color-primary: #0a9342;
--color-light: #598EB5; --color-light: #31cc72;
--color-lighter: #5F98C2; --color-lighter: #35db7a;
--color-dark: #425d79; --color-dark: #087536;
--color-darker: #274a65; --color-darker: #075728;
--color-link: var(--color-dark); --color-link: var(--color-font);
--color-link-hover: var(--color-darker); --color-link-hover: var(--color-font);
--color-lmu-box-border: var(--color-lightwhite);
&.theme--lavender {
--color-primary: #4C7A9C;
--color-light: #598EB5;
--color-lighter: #5F98C2;
--color-dark: #425d79;
--color-darker: #274a65;
--color-link: var(--color-dark);
--color-link-hover: var(--color-darker);
}
&.theme--neutral-blue { &.theme--neutral-blue {
--color-primary: #3E606F; --color-primary: #3E606F;
@ -64,7 +74,7 @@ body {
--color-darker: #2E112D; --color-darker: #2E112D;
} }
&.theme--mint-green { &.theme--moss-green {
--color-primary: #5C996B; --color-primary: #5C996B;
--color-light: #7ACC8F; --color-light: #7ACC8F;
--color-lighter: #99FFB2; --color-lighter: #99FFB2;
@ -159,7 +169,6 @@ h4 {
.main__content { .main__content {
position: relative; position: relative;
background-color: white; background-color: white;
z-index: 0;
overflow: hidden; overflow: hidden;
> .container { > .container {
@ -281,38 +290,31 @@ a.btn.btn-info:hover,
} }
/* TABLE DESIGN */ /* TABLE DESIGN */
.table__row { .table__td, .table__th {
padding-top: 14px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
max-width: 300px;
}
/* TODO: move outside of table__row as soon as tds and ths get their own class */ .table__td {
/* .table__td, .table__th { */ font-size: 16px;
td, th { color: var(--color-font);
padding-top: 14px; line-height: 1.4;
padding-bottom: 10px; vertical-align: top;
padding-left: 10px; }
padding-right: 10px;
max-width: 300px;
}
/* .table__td { */ .table__th {
td { background-color: var(--color-dark);
font-size: 16px; position: relative;
color: #808080; font-size: 16px;
line-height: 1.4; color: #fff;
vertical-align: top; line-height: 1.4;
} padding-top: 10px;
padding-bottom: 10px;
/* .table__th { */ font-weight: bold;
th { text-align: left;
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) { @media (max-width: 1200px) {
@ -323,7 +325,7 @@ a.btn.btn-info:hover,
} }
.table__td-content { .table__td-content {
max-height: 100px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
} }

View File

@ -0,0 +1,5 @@
_{MsgSubmissionsAlreadyAssigned (genericLength alreadyAssigned')}
<ul>
$forall (cID, _) <- alreadyAssigned'
<li><pre>#{toPathPiece cID}

View File

@ -0,0 +1,5 @@
_{MsgCouldNotAssignCorrectorsAuto (genericLength unassigned')}
<ul>
$forall cID <- unassigned'
<li><pre>#{toPathPiece cID}

View File

@ -3,71 +3,71 @@
<div .scrolltable> <div .scrolltable>
<table .table.table--striped.table--hover.table--vertical> <table .table.table--striped.table--hover.table--vertical>
<tr.table__row> <tr.table__row>
<th> _{MsgName} <th .table__th> _{MsgName}
<td> #{display userDisplayName} <td .table__td> #{display userDisplayName}
<tr.table__row> <tr.table__row>
<th> _{MsgMatrikelNr} <th .table__th> _{MsgMatrikelNr}
<td> #{display userMatrikelnummer} <td .table__td> #{display userMatrikelnummer}
<tr.table__row> <tr.table__row>
<th> _{MsgEMail} <th .table__th> _{MsgEMail}
<td> #{display userEmail} <td .table__td> #{display userEmail}
<tr.table__row> <tr.table__row>
<th> _{MsgIdent} <th .table__th> _{MsgIdent}
<td> #{display userIdent} <td .table__td> #{display userIdent}
<tr.table__row> <tr.table__row>
<th> _{MsgPlugin} <th .table__th> _{MsgPlugin}
<td> #{display userPlugin} <td .table__td> #{display userPlugin}
$if not $ null admin_rights $if not $ null admin_rights
<tr.table__row> <tr.table__row>
<th> Administrator <th .table__th> Administrator
<td> <td .table__td>
<ul> <ul>
$forall institute <- admin_rights $forall institute <- admin_rights
<li>#{display institute} <li>#{display institute}
$if not $ null lecturer_rights $if not $ null lecturer_rights
<tr.table__row> <tr.table__row>
<th> Lehrberechtigt <th .table__th> Lehrberechtigt
<td> <td .table__td>
<ul> <ul>
$forall institute <- lecturer_rights $forall institute <- lecturer_rights
<li>#{display institute} <li>#{display institute}
$if not $ null lecture_owner $if not $ null lecture_owner
<tr.table__row> <tr.table__row>
<th> Eigene Kurse <th .table__th> Eigene Kurse
<td> <td .table__td>
<ul> <ul>
$forall (E.Value csh, E.Value tid) <- lecture_owner $forall (E.Value csh, E.Value tid) <- lecture_owner
<li> <li>
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh} <a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
$if not $ null lecture_corrector $if not $ null lecture_corrector
<tr.table__row> <tr.table__row>
<th> Korrektor <th .table__th> Korrektor
<td> <td .table__td>
<ul> <ul>
$forall (E.Value csh, E.Value tid) <- lecture_corrector $forall (E.Value csh, E.Value tid) <- lecture_corrector
<li> <li>
<a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh} <a href=@{CourseR tid csh CShowR}>#{display tid} - #{csh}
$if not $ null studies $if not $ null studies
<tr.table__row> <tr.table__row>
<th> Studiengänge <th .table__th> Studiengänge
<td> <td .table__td>
<table .table .table-striped .table-hover> <table .table .table-striped .table-hover>
<tr.table__row> <tr.table__row>
<th> Abschluss <th .table__th> Abschluss
<th> Studiengang <th .table__th> Studiengang
<th> Studienart <th .table__th> Studienart
<th> Semester <th .table__th> Semester
$forall (degree,field,fieldtype,semester) <- studies $forall (degree,field,fieldtype,semester) <- studies
<tr.table__row> <tr.table__row>
<td> #{display degree} <td .table__td> #{display degree}
<td> #{display field} <td .table__td> #{display field}
<td> #{display fieldtype} <td .table__td> #{display fieldtype}
<td> #{display semester} <td .table__td> #{display semester}
$if not $ null participant $if not $ null participant
<tr.table__row> <tr.table__row>
<th> Teilnehmer <th .table__th> Teilnehmer
<td> <td .table__td>
<ul> <ul>
$forall (E.Value csh, E.Value tid, regSince) <- participant $forall (E.Value csh, E.Value tid, regSince) <- participant
<li> <li>

17
templates/profile.julius Normal file
View File

@ -0,0 +1,17 @@
document.addEventListener('DOMContentLoaded', function () {
var themeSelector = document.querySelector('[placeholder="theme-select"]');
themeSelector.addEventListener('change', function() {
// get rid of old themes on body
var options = Array.from(themeSelector.options)
.forEach(function (option) {
document.body.classList.remove(optionToTheme(option));
});
// add newly selected theme
document.body.classList.add(optionToTheme(themeSelector.options[themeSelector.value - 1]));
});
function optionToTheme(option) {
return optionValue = 'theme--' + option.innerText.toLowerCase().trim().replace(/\s/g, '-');
}
});

View File

@ -11,6 +11,13 @@
alertEl.classList.add('alert--invisible'); alertEl.classList.add('alert--invisible');
}); });
alertEl.insertBefore(closeEl, alertEl.children[0]); alertEl.insertBefore(closeEl, alertEl.children[0]);
// auto-hide info and success-alerts after 3 seconds
if (!alertEl.matches('.alert-danger, .alert-warning')) {
window.setTimeout(function() {
alertEl.classList.add('alert--invisible');
}, 3000);
}
} }
})(); })();

View File

@ -8,6 +8,15 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@media (min-width: 768px) {
.alerts {
top: 150px;
bottom: auto;
}
}
.alert { .alert {
position: relative; position: relative;
display: inline-block; display: inline-block;
@ -23,6 +32,7 @@
margin-left: 20px; margin-left: 20px;
margin-right: 60px; margin-right: 60px;
box-shadow: 0 0 7px var(--color-dark); box-shadow: 0 0 7px var(--color-dark);
animation: slide-in-alert .2s ease-out forwards;
+ .alert:not(.alert--invisible) { + .alert:not(.alert--invisible) {
margin-top: 20px; margin-top: 20px;
@ -39,6 +49,15 @@
} }
} }
@keyframes slide-in-alert {
from {
left: 120%;
}
to {
left: 0;
}
}
@media (min-width: 425px) { @media (min-width: 425px) {
.alert { .alert {
@ -91,6 +110,7 @@
align-items: center; align-items: center;
font-size: 40px; font-size: 40px;
color: var(--color-dark); color: var(--color-dark);
box-shadow: 0 0 2px var(--color-dark);
} }
} }
@ -146,6 +166,7 @@
&::after { &::after {
content: '\f05a'; content: '\f05a';
color: var(--color-success); color: var(--color-success);
box-shadow: 0 0 2px var(--color-success);
} }
} }
} }
@ -173,6 +194,7 @@
&::after { &::after {
content: '\f071'; content: '\f071';
color: var(--color-warning); color: var(--color-warning);
box-shadow: 0 0 2px var(--color-warning);
} }
} }
} }
@ -189,7 +211,7 @@
/* TODO: remove me as soon as messagerenderer-error in julius gets resolved */ /* TODO: remove me as soon as messagerenderer-error in julius gets resolved */
&::before { &::before {
color: var(--color-lightwhite); color: var(--color-lightwhite);
} }
} }
.alert__content { .alert__content {
@ -201,6 +223,7 @@
&::after { &::after {
content: '\f071'; content: '\f071';
color: var(--color-error); color: var(--color-error);
box-shadow: 0 0 2px var(--color-error);
} }
} }
} }

View File

@ -0,0 +1,3 @@
<td .table__td *{attrs}>
<div .table__td-content>
^{widget}

View File

@ -0,0 +1,11 @@
<th .table__th *{attrs} :isSortable:.sortable :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc>
$maybe flag <- sortableKey
$case directions
$of [SortAsc]
<a .table__th-link href=#{tblLink $ setParam (wIdent "sorting") (Just $ CI.foldedCase flag <> "-desc")}>
^{widget}
$of _
<a .table__th-link href=#{tblLink $ setParam (wIdent "sorting") (Just $ CI.foldedCase flag <> "-asc")}>
^{widget}
$nothing
^{widget}

View File

@ -0,0 +1,2 @@
<a href=@{route}>
^{widget}

View File

@ -1,18 +1,20 @@
$newline never $newline never
<table id="#{dbtIdent}" .table.table--striped.table--hover> <table id="#{dbtIdent}" .table.table--striped.table--hover>
$maybe sortableP <- pSortable $maybe wHeaders' <- wHeaders
$with toSortable <- toSortable sortableP <thead>
<thead> <tr .table__row.table__row--head>
<tr .table__row> $forall widget <- wHeaders'
$forall OneColonnade{..} <- getColonnade dbtColonnade $# cell/header.hamlet
<!-- TODO: give ths a class 'table__th' --> ^{widget}
<!-- TODO: wrap content of th in 'div.table__th-content' -->
^{widgetFromCell th $ withSortLinks $ toSortable oneColonnadeHead}
$nothing $nothing
<tbody> <tbody>
$forall row <- rows $if null wRows
<tr .table__row> <tr>
$forall OneColonnade{..} <- getColonnade dbtColonnade <td colspan=#{show columnCount}>
<!-- TODO: give tds a class 'table__td' --> Kein Inhalt.
<!-- TODO: wrap content of td in 'div.table__td-content' --> $else
^{widgetFromCell td $ oneColonnadeEncode row} $forall row <- wRows
<tr .table__row>
$forall widget <- row
$# cell/body.hamlet
^{widget}

View File

@ -1,44 +1,40 @@
/* SORTABLE TABLE */ /* SORTABLE TABLE-HEADERS*/
.table { .table__th.sortable {
position: relative;
/* TODO: move outside of table as soon as tds and ths get their own class */ padding-right: 24px;
th.sortable { cursor: pointer;
position: relative; }
padding-right: 24px;
cursor: pointer; .table__th.sortable::after,
} .table__th.sortable::before {
content: '';
th.sortable::after, position: absolute;
th.sortable::before { top: 50%;
content: ''; right: 4px;
position: absolute; width: 0;
top: 50%; height: 0;
right: 4px; border-left: 8px solid transparent;
width: 0; border-right: 8px solid transparent;
height: 0; border-bottom: 8px solid rgba(255, 255, 255, 0.4);
border-left: 8px solid transparent; }
border-right: 8px solid transparent;
border-bottom: 8px solid rgba(255, 255, 255, 0.4); .table__th.sortable::before {
} /* magic numbers to move arrow back in the right position after flipping it.
this allows us to use the same border for the up and the down arrow */
th.sortable::before { transform: translateY(150%) scale(1, -1);
/* magic numbers to move arrow back in the right position after flipping it. transform-origin: top;
this allows us to use the same border for the up and the down arrow */ }
transform: translateY(150%) scale(1, -1);
transform-origin: top; .table__th.sortable::after {
} transform: translateY(-150%);
}
th.sortable::after {
transform: translateY(-150%); .table__th.sortable:hover::before,
} .table__th.sortable:hover::after {
border-bottom-color: rgba(255, 255, 255, 0.7);
th.sortable:hover::before, }
th.sortable:hover::after {
border-bottom-color: rgba(255, 255, 255, 0.7); .table__th.sorted-asc::before,
} .table__th.sorted-desc::after {
border-bottom-color: white !important;
th.sorted-asc::before,
th.sorted-desc::after {
border-bottom-color: white !important;
}
} }

View File

@ -48,9 +48,15 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
&:not(.asidenav__list-item--active):hover {
background-color: var(--color-darker);
> .asidenav__link-wrapper {
color: var(--color-lightwhite);
}
}
&:hover { &:hover {
color: var(--color-link);
background-color: var(--color-lightwhite);
.asidenav__link-shorthand { .asidenav__link-shorthand {
transform: scale(1.05, 1.0); transform: scale(1.05, 1.0);
@ -58,10 +64,6 @@
text-shadow: none; text-shadow: none;
} }
> .asidenav__link-wrapper {
color: var(--color-link);
}
.asidenav__nested-list { .asidenav__nested-list {
transform: translateX(100%); transform: translateX(100%);
opacity: 1; opacity: 1;
@ -109,7 +111,6 @@
width: 50px; width: 50px;
} }
.glyphicon + .asidenav__link-label { .glyphicon + .asidenav__link-label {
padding-left: 0; padding-left: 0;
} }
@ -143,15 +144,10 @@
background-color: var(--color-dark); background-color: var(--color-dark);
&:hover { &:hover {
color: var(--color-link); background-color: var(--color-darker);
background-color: var(--color-lightwhite);
.asidenav__link-wrapper {
background-color: white;
color: var(--color-link);
}
} }
.asidenav__link-wrapper {
.asidenav__link-wrapper {
padding-left: 13px; padding-left: 13px;
padding-right: 13px; padding-right: 13px;
border-left: 20px solid white; border-left: 20px solid white;

View File

@ -7,5 +7,4 @@ $case formLayout
$if not (Blaze.null $ fvLabel view) $if not (Blaze.null $ fvLabel view)
<label .form-group__label for=#{fvId view}>#{fvLabel view} <label .form-group__label for=#{fvId view}>#{fvLabel view}
<div .form-group__input> <div .form-group__input>
$# FIXME: file-input does not have `required` attribute, although set on form-group
^{fvInput view} ^{fvInput view}

View File

@ -10,6 +10,12 @@
if (requireds.length == 0) { if (requireds.length == 0) {
return false; return false;
} }
if (typeof button.dataset.formnorequired !== 'undefined' && button.dataset.formnorequired !== null) {
button.addEventListener('click', function() {
form.submit();
});
return false;
}
updateButtonState(); updateButtonState();
requireds.forEach(function(el) { requireds.forEach(function(el) {
@ -28,17 +34,52 @@
} }
} }
}; };
window.utils.interactiveFieldset = function(form, fieldSets) {
var fields = fieldSets.map(function(fs) {
return {
fieldSet: fs,
condId: fs.dataset.conditionalId,
condValue: fs.dataset.conditionalValue,
condEl: form.querySelector('#' + fs.dataset.conditionalId),
};
}).filter(function(field) {
return !!field.condEl;
});
function updateFields() {
fields.forEach(function(field) {
field.fieldSet.classList.toggle('hidden', field.condEl.value !== field.condValue);
});
}
function addEventListeners() {
fields.forEach(function(field) {
field.condEl.addEventListener('input', updateFields)
});
}
if (fieldSets.length) {
addEventListeners();
updateFields();
}
};
})(); })();
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// auto reactiveButton submit-buttons with required fields
var forms = document.querySelectorAll('form'); var forms = document.querySelectorAll('form');
Array.from(forms).forEach(function(form) { Array.from(forms).forEach(function(form) {
var submitBtn = form.querySelector('[type=submit]'); // auto reactiveButton submit-buttons with required fields
if (submitBtn) { var submitBtns = Array.from(form.querySelectorAll('[type=submit]'));
submitBtns.forEach(function(submitBtn) {
window.utils.reactiveButton(form, submitBtn, validateForm); window.utils.reactiveButton(form, submitBtn, validateForm);
} });
// auto conditonal fieldsets
var fieldSets = Array.from(form.querySelectorAll('fieldset[data-conditional-id][data-conditional-value]'));
window.utils.interactiveFieldset(form, fieldSets);
}); });
function validateForm(inputs) { function validateForm(inputs) {

View File

@ -0,0 +1,15 @@
.hidden {
visibility: hidden;
height: 0;
opacity: 0;
}
fieldset {
border: 0;
margin: 20px 0 30px;
legend {
display: none;
}
}

View File

@ -0,0 +1,4 @@
<fieldset data-conditional-id="#{fvId actionView}" data-conditional-value="#{toPathPiece act}">
<legend>
_{act}
^{w}

View File

@ -0,0 +1,4 @@
^{fvInput actionView}
$forall w <- actionWidgets
^{w}

View File

@ -4,7 +4,7 @@ $newline never
<a href="/" .navbar__logo> <a href="/" .navbar__logo>
<ul .navbar__list.list--inline> <ul .navbar__list.list--inline.navbar__list-left>
$forall menuType <- menuTypes $forall menuType <- menuTypes
$case menuType $case menuType
$of NavbarAside (MenuItem label mIcon route _) $of NavbarAside (MenuItem label mIcon route _)

View File

@ -3,11 +3,10 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-start;
width: 100%; width: 100%;
height: var(--header-height); height: var(--header-height);
padding-right: 2vw; padding-right: 2vw;
padding-left: calc(24% + 40px);
background: var(--color-darker); /* Old browsers */ background: var(--color-darker); /* Old browsers */
background: -moz-linear-gradient(bottom, var(--color-dark) 0%, var(--color-darker) 100%); /* FF3.6-15 */ 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 */ background: -webkit-linear-gradient(bottom, var(--color-dark) 0%,var(--color-darker) 100%); /* Chrome10-25,Safari5.1-6 */
@ -21,13 +20,6 @@
transition: all .2s cubic-bezier(0.03, 0.43, 0.58, 1); transition: all .2s cubic-bezier(0.03, 0.43, 0.58, 1);
} }
@media (min-width: 1200px) {
.navbar {
padding-left: 340px;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.navbar { .navbar {
@ -36,31 +28,67 @@
} }
.navbar__logo { .navbar__logo {
position: absolute; top: 10px;
top: 15px;
left: 20px; left: 20px;
transition: all .2s ease; height: 80px;
transform-origin: left; padding: 0 20px;
width: 0px; display: flex;
flex-basis: 300px;
font-size: 16px;
align-items: center;
color: var(--color-lightwhite); color: var(--color-lightwhite);
transform-origin: left;
transition: all .2s ease;
&:hover { &:hover {
color: var(--color-lightwhite); color: var(--color-lightwhite);
} }
&::before { &::before {
content: 'Uni2work'; content: 'LMU';
font-family: var(--font-logo); font-family: var(--font-logo);
font-size: 42px;
font-weight: bold; font-weight: bold;
letter-spacing: 2px; letter-spacing: 2px;
display: flex;
align-items: flex-end;
font-size: 30px;
min-width: 70px;
height: calc(100% - 4px);
padding: 0 6px 4px;
box-shadow: 0 0 0 1px inset var(--color-lmu-box-border);
}
&::after {
content: 'Uni2work';
margin-left: 12px;
font-weight: normal;
letter-spacing: 2px;
display: flex;
align-items: flex-end;
text-transform: uppercase;
width: 100%;
height: calc(100% - 4px);
padding: 0 6px 4px;
box-shadow: 0 0 0 1px inset var(--color-lmu-box-border);
}
}
@media (max-width: 1200px) {
.navbar__logo {
flex-basis: 24%;
font-size: 16px;
} }
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
.navbar__logo { .navbar__logo {
transform: scale(0.7); font-size: 14px;
&::before {
content: none;
}
} }
} }
@ -75,21 +103,30 @@
.navbar__link-wrapper { .navbar__link-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: flex-end;
align-items: center; align-items: flex-start;
height: var(--header-height); height: 80px;
min-width: 90px;
color: var(--color-lightwhite); color: var(--color-lightwhite);
transition: height .2s cubic-bezier(0.03, 0.43, 0.58, 1); transition: height .2s cubic-bezier(0.03, 0.43, 0.58, 1);
box-shadow: 0 0 0 1px inset var(--color-lmu-box-border);
} }
.navbar__link-label { .navbar__link-label {
transition: opacity .2s ease; transition: opacity .2s ease;
padding: 0 13px; padding: 4px 6px;
text-transform: uppercase; text-transform: uppercase;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.navbar__link-wrapper {
box-shadow: none;
min-width: 0;
align-items: center;
justify-content: center;
}
.navbar__link-label { .navbar__link-label {
padding: 0 7px; padding: 0 7px;
} }
@ -105,6 +142,10 @@
/* navbar list */ /* navbar list */
.navbar__list { .navbar__list {
white-space: nowrap; white-space: nowrap;
+ .navbar__list {
margin-left: 12px;
}
} }
/* list item */ /* list item */
@ -112,6 +153,10 @@
position: relative; position: relative;
transition: background-color .1s ease; transition: background-color .1s ease;
+ .navbar__list-item {
margin-left: 12px;
}
.glyphicon { .glyphicon {
position: relative; position: relative;
width: 20px; width: 20px;
@ -120,10 +165,22 @@
.glyphicon::before { .glyphicon::before {
height: 20px; height: 20px;
margin-left: 6px;
} }
.fas { .fas {
height: 20px; height: 20px;
margin-left: 8px;
}
}
@media (max-width: 768px) {
.navbar__list-item {
.fas {
margin-left: 0;
}
} }
} }
@ -134,15 +191,36 @@
} }
} }
.navbar__list-left {
flex: 5;
padding-left: 40px;
}
@media (max-width: 768px) {
.navbar__list-left {
padding-left: 0;
}
}
.navbar__list-item--secondary { .navbar__list-item--secondary {
margin-left: 20px; margin-left: 20px;
color: var(--color-grey); color: var(--color-grey);
.navbar__link-wrapper {
color: var(--color-grey);
box-shadow: 0 0 0 1px inset var(--color-grey);
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.navbar__list-item--secondary { .navbar__list-item--secondary {
margin-left: 0; margin-left: 0;
.navbar__link-wrapper {
box-shadow: none;
}
} }
} }
@ -169,10 +247,6 @@
color: var(--color-lightwhite); color: var(--color-lightwhite);
} }
.navbar__list-item--secondary .navbar__link-wrapper {
color: var(--color-grey);
}
/* sticky state */ /* sticky state */
.navbar--sticky { .navbar--sticky {
height: var(--header-height-collapsed); height: var(--header-height-collapsed);
@ -209,7 +283,7 @@
} }
} }
@media (max-height: 768px) { @media (max-height: 500px) {
.navbar, .navbar,
.navbar__pushdown { .navbar__pushdown {
@ -222,5 +296,6 @@
.navbar__logo { .navbar__logo {
top: 5px; top: 5px;
height: var(--header-height-collapsed);
} }
} }