Merge branch '740-labels' into 'master'
Resolve "Labels" See merge request uni2work/uni2work!74
This commit is contained in:
commit
fa0744172e
@ -232,6 +232,8 @@ user-defaults:
|
||||
download-files: false
|
||||
warning-days: 1209600
|
||||
show-sex: false
|
||||
exam-office-get-synced: true
|
||||
exam-office-get-labels: true
|
||||
|
||||
# During central allocations lecturer-given ratings of applications (as
|
||||
# ExamGrades) are combined with a central priority.
|
||||
|
||||
@ -10,6 +10,8 @@
|
||||
--color-success-dark: #1ca64c
|
||||
--color-info: #c4c4c4
|
||||
--color-info-dark: #919191
|
||||
--color-nonactive: #efefef
|
||||
--color-nonactive-dark: #9a989e
|
||||
--color-lightblack: #1A2A36
|
||||
--color-lightwhite: #fcfffa
|
||||
--color-grey: #B1B5C0
|
||||
@ -740,6 +742,9 @@ section
|
||||
.notification-success
|
||||
color: var(--color-success-dark)
|
||||
|
||||
.notification-nonactive
|
||||
color: var(--color-nonactive)
|
||||
|
||||
// "Heated" element.
|
||||
// Set custom property "--hotness" to a value from 0 to 1 to turn
|
||||
// the element's background to a color on a gradient from green to red.
|
||||
@ -1476,6 +1481,9 @@ a.breadcrumbs__home
|
||||
|
||||
&--success
|
||||
border-left-color: var(--color-success)
|
||||
|
||||
&--disabled
|
||||
border-left-color: var(--color-nonactive)
|
||||
|
||||
|
||||
.active-allocations__wrapper
|
||||
@ -1737,3 +1745,28 @@ video
|
||||
font-size: .5em
|
||||
font-family: var(--font-monospace)
|
||||
color: var(--color-fontsec)
|
||||
|
||||
.exam-office-label
|
||||
--lbl-padding-vert: 5px
|
||||
--lbl-padding-hori: 15px
|
||||
padding: var(--lbl-padding-vert) var(--lbl-padding-hori)
|
||||
border-radius: 20px 20px 20px 20px
|
||||
font-weight: 600
|
||||
text-align: center
|
||||
width: fit-content
|
||||
margin: 0 auto
|
||||
&.success
|
||||
background-color: var(--color-success-dark)
|
||||
color: var(--color-lightwhite)
|
||||
&.error
|
||||
background-color: var(--color-error)
|
||||
color: var(--color-lightwhite)
|
||||
&.warning
|
||||
background-color: var(--color-warning)
|
||||
color: var(--color-lightwhite)
|
||||
&.info
|
||||
background-color: var(--color-lightblack)
|
||||
color: var(--color-lightwhite)
|
||||
&.nonactive
|
||||
background-color: var(--color-nonactive)
|
||||
color: var(--color-nonactive-dark)
|
||||
|
||||
@ -55,3 +55,24 @@ ExamOfficeFieldSubscribed: Abboniert
|
||||
UtilExamClosed: Noten gemeldet
|
||||
ExamFinishedOffice: Noten bekannt gegeben
|
||||
ExamOfficeFieldForced: Forcierte Einsicht
|
||||
|
||||
ExamOfficeGetSynced: Synchronisiert-Status in Prüfungsliste anzeigen
|
||||
ExamOfficeGetSyncedTip: Soll unter „Prüfungen“ der Synchronisiert-Status zu jeder Prüfung angezeigt werden? (Ein Deaktivieren dieser Option kann zu kürzeren Ladezeiten der Prüfungsliste führen.)
|
||||
|
||||
ExamLabel: Prüfungs-Label
|
||||
ExamOfficeGetLabels: Labels in Prüfungsliste anzeigen
|
||||
ExamOfficeGetLabelsTip: Sollen unter „Prüfungen“ die gesetzten Labels zu jeder Prüfung angezeigt werden?
|
||||
ExamOfficeLabels: Prüfungs-Labels
|
||||
ExamOfficeLabelsTip: Sie können hier Labels anlegen und verwalten, welche sie einzelnen Prüfungen über die Prüfungsliste (siehe „Prüfungen“) zuweisen können.
|
||||
ExamOfficeLabelName !ident-ok: Name
|
||||
ExamOfficeLabelStatus !ident-ok: Status
|
||||
ExamOfficeLabelPriority: Priorität
|
||||
ExamOfficeLabelAlreadyExists: Es existiert bereits ein Prüfungs-Label mit diesem Namen!
|
||||
ExamOfficeExamsNoLabel: Kein Label
|
||||
ExamSetLabel: Label setzen
|
||||
ExamLabelsSet n@Int: #{n} Prüfungs-#{pluralDE n "Label" "Labels"} gesetzt
|
||||
ExamRemoveLabel: Label entfernen
|
||||
ExamLabelsRemoved n@Int: #{n} Prüfungs-#{pluralDE n "Label" "Labels"} entfernt
|
||||
ExamOfficeLabelSetLabelOnExport: Prüfungs-Label beim Export setzen
|
||||
ExamOfficeLabelSetLabelOnExportTip t@Text: Soll beim CSV-Export automatisch das Export-Label für die jeweilige Prüfung gesetzt werden? Von Ihnen gesetzte Prüfungs-Label sind ausschließlich für Sie sichtbar und können von jedem Prüfungsbeauftragten unabhängig voneinander verwaltet bzw. verwendet werden. Ihr aktuell für den CSV-Export eingestelltes Prüfungs-Label ist „#{t}“. Sie können das zu setzende Prüfungs-Label unter „Export-Optionen“ oder in Ihren persönlichen Benutzereinstellungen ändern.
|
||||
ExamOfficeLabelSetLabelOnExportForcedTip: Soll beim CSV-Export automatisch das Export-Label für die jeweilige Prüfung gesetzt werden? Von Ihnen gesetzte Prüfungs-Label sind ausschließlich für Sie sichtbar und können von jedem Prüfungsbeauftragten unabhängig voneinander verwaltet bzw. verwendet werden. Sie haben aktuell kein Export-Label festgelegt und können diese Option daher nicht auswählen. Sie können das beim CSV-Export zu setzende Prüfungs-Label unter „Export-Optionen“ oder in Ihren persönlichen Benutzereinstellungen wählen.
|
||||
|
||||
@ -53,3 +53,24 @@ ExamOfficeFieldSubscribed: subscribed
|
||||
UtilExamClosed: Exam achievements registered
|
||||
ExamFinishedOffice: Exam achievements published
|
||||
ExamOfficeFieldForced: Forced access
|
||||
|
||||
ExamOfficeGetSynced: Show synchronised status in exam list
|
||||
ExamOfficeGetSyncedTip: Should the synchronised status be displayed in “Exams”? (Disabling this option may lead to shorter loading times of the exam list.)
|
||||
|
||||
ExamLabel: Exam label
|
||||
ExamOfficeGetLabels: Show labels in exam list
|
||||
ExamOfficeGetLabelsTip: Should the labels of each exam be displayed in “Exams”?
|
||||
ExamOfficeLabels: Exam labels
|
||||
ExamOfficeLabelsTip: Here you can add and manage labels, which you can assign exam list entries (see “Exams”).
|
||||
ExamOfficeLabelName: Name
|
||||
ExamOfficeLabelStatus: Status
|
||||
ExamOfficeLabelPriority: Priority
|
||||
ExamOfficeLabelAlreadyExists: There already exists an exam label with this name!
|
||||
ExamOfficeExamsNoLabel: No label
|
||||
ExamSetLabel: Set label
|
||||
ExamLabelsSet n: Successfully set #{n} exam #{pluralEN n "label" "labels"}
|
||||
ExamRemoveLabel: Remove label
|
||||
ExamLabelsRemoved n: Successfully removed #{n} exam #{pluralEN n "label" "labels"}
|
||||
ExamOfficeLabelSetLabelOnExport: Set exam label while exporting
|
||||
ExamOfficeLabelSetLabelOnExportTip t: Should the export label be set for the respective exam? Your set exam labels are exclusively visible to you and may be managed and used by each exam office member independently. Your saved exam label for CSV export is currently “#{t}”. You can change the exam label set while exporting under “Export options” or in your user settings.
|
||||
ExamOfficeLabelSetLabelOnExportForcedTip: Should the export label be set for the respective exam? Your set exam labels are exclusively visible to you and may be managed and used by each exam office member independently. You do not currently have any exam label selected as export label and therefor cannot active this setting. To set an exam label as export label, go to “Export options” or your user settings.
|
||||
|
||||
@ -3,6 +3,8 @@ CsvOptionsTip: Diese Einstellungen betreffen primär den CSV-Export; beim Import
|
||||
CsvFormatOptions: Dateiformat
|
||||
CsvTimestamp: Zeitstempel
|
||||
CsvTimestampTip: Soll an den Namen jeder exportierten CSV-Datei ein Zeitstempel vorne angehängt werden?
|
||||
CsvExportLabel: Prüfungs-Label bei Export
|
||||
CsvExportLabelTip: Soll beim CSV-Export von Prüfungsleistungen automatisch ein gegebenes Label für diese Prüfung gesetzt werden?
|
||||
CsvPresetRFC: Standard-Konform (RFC 4180)
|
||||
CsvPresetExcel: Excel-Kompatibel
|
||||
CsvCustom: Benutzerdefiniert
|
||||
|
||||
@ -3,6 +3,8 @@ CsvOptionsTip: These settings primarily affect CSV export. During import most se
|
||||
CsvFormatOptions: File format
|
||||
CsvTimestamp: Timestamp
|
||||
CsvTimestampTip: Should the name of every exported csv file contain a timestamp?
|
||||
CsvExportLabel: Exam label on export
|
||||
CsvExportLabelTip: Should a given label be automatically set for an exam of which results are exported to CSV?
|
||||
CsvPresetRFC: Standards-compliant (RFC 4180)
|
||||
CsvPresetExcel: Excel compatible
|
||||
CsvCustom: User defined
|
||||
|
||||
@ -112,3 +112,5 @@ AllocNotifyNewCourseDefault: Systemweite Einstellung
|
||||
AllocNotifyNewCourseForceOff: Nein
|
||||
AllocNotifyNewCourseForceOn: Ja
|
||||
Settings: Individuelle Benutzereinstellungen
|
||||
|
||||
FormExamOffice: Prüfungsverwaltung
|
||||
|
||||
@ -112,4 +112,6 @@ LanguageChanged: Language changed successfully
|
||||
AllocNotifyNewCourseDefault: System-wide setting
|
||||
AllocNotifyNewCourseForceOff: No
|
||||
AllocNotifyNewCourseForceOn: Yes
|
||||
Settings: Settings
|
||||
Settings: Settings
|
||||
|
||||
FormExamOffice: Exam Office
|
||||
@ -22,6 +22,7 @@ TableExamName !ident-ok: Name
|
||||
TableExamTime: Termin
|
||||
TableExamRegistration: Prüfungsanmeldung
|
||||
TableExamResult: Prüfungsergebnis
|
||||
TableExamLabel !ident-ok: Label
|
||||
TableSheet: Blatt
|
||||
TableLastEdit: Letzte Änderung
|
||||
TableSubmission: Abgabenummer
|
||||
@ -61,4 +62,7 @@ SelectColumn: Auswahl
|
||||
CsvExport: CSV-Export
|
||||
TableProportion c@Text of'@Text prop@Rational !ident-ok: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
|
||||
TableProportionNoRatio c@Text of'@Text !ident-ok: #{c}/#{of'}
|
||||
TableExamFinished: Ergebnisse sichtbar ab
|
||||
TableExamFinished: Ergebnisse sichtbar ab
|
||||
TableExamOfficeLabel: Label-Name
|
||||
TableExamOfficeLabelStatus: Label-Farbe
|
||||
TableExamOfficeLabelPriority: Label-Priorität
|
||||
@ -22,6 +22,7 @@ TableExamName: Name
|
||||
TableExamTime: Time
|
||||
TableExamRegistration: Exam registration
|
||||
TableExamResult: Exam result
|
||||
TableExamLabel: Label
|
||||
TableSheet: Sheet
|
||||
TableLastEdit: Latest edit
|
||||
TableSubmission: Submission-number
|
||||
@ -61,4 +62,7 @@ SelectColumn: Selection
|
||||
CsvExport: CSV export
|
||||
TableProportion c of' prop: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
|
||||
TableProportionNoRatio c of': #{c}/#{of'}
|
||||
TableExamFinished: Results visible from
|
||||
TableExamFinished: Results visible from
|
||||
TableExamOfficeLabel: Label name
|
||||
TableExamOfficeLabelStatus: Label colour
|
||||
TableExamOfficeLabelPriority: Label priority
|
||||
@ -136,6 +136,7 @@ MessageError: Fehler
|
||||
MessageWarning: Warnung
|
||||
MessageInfo !ident-ok: Information
|
||||
MessageSuccess: Erfolg
|
||||
MessageNonactive: Inaktiv
|
||||
ShortFieldPrimary: HF
|
||||
ShortFieldSecondary: NF
|
||||
SheetGradingPassPoints': Bestehen nach Punkten
|
||||
|
||||
@ -136,6 +136,7 @@ MessageError: Error
|
||||
MessageWarning: Warning
|
||||
MessageInfo: Information
|
||||
MessageSuccess: Success
|
||||
MessageNonactive: Inactive
|
||||
ShortFieldPrimary: Mj
|
||||
ShortFieldSecondary: Mn
|
||||
SheetGradingPassPoints': Passing by points
|
||||
|
||||
18
models/exam-office/exam-labels.model
Normal file
18
models/exam-office/exam-labels.model
Normal file
@ -0,0 +1,18 @@
|
||||
ExamOfficeLabel
|
||||
user UserId
|
||||
name ExamOfficeLabelName
|
||||
status MessageStatus
|
||||
priority Int -- determines label ordering
|
||||
UniqueExamOfficeLabel user name
|
||||
deriving Generic
|
||||
|
||||
ExamOfficeExamLabel
|
||||
exam ExamId
|
||||
label ExamOfficeLabelId
|
||||
UniqueExamOfficeExamLabel exam
|
||||
deriving Generic
|
||||
ExamOfficeExternalExamLabel
|
||||
externalExam ExternalExamId
|
||||
label ExamOfficeLabelId
|
||||
UniqueExamOfficeExternalExamLabel externalExam
|
||||
deriving Generic
|
||||
@ -35,6 +35,8 @@ User json -- Each Uni2work user has a corresponding row in this table; create
|
||||
csvOptions CsvOptions "default='{}'::jsonb"
|
||||
sex Sex Maybe
|
||||
showSex Bool default=false
|
||||
examOfficeGetSynced Bool default=true -- whether synced status should be displayed for exam results by default
|
||||
examOfficeGetLabels Bool default=true -- whether labels should be displayed for exam results by default
|
||||
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
|
||||
UniqueEmail email -- Column 'email' can be used as a row-key in this table
|
||||
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
|
||||
@ -53,8 +55,8 @@ UserSystemFunction
|
||||
UniqueUserSystemFunction user function
|
||||
deriving Generic
|
||||
UserExamOffice
|
||||
user UserId
|
||||
field StudyTermsId
|
||||
user UserId
|
||||
field StudyTermsId
|
||||
UniqueUserExamOffice user field
|
||||
deriving Generic
|
||||
UserSchool -- Managed by users themselves, encodes "schools of interest"
|
||||
|
||||
2
routes
2
routes
@ -112,7 +112,7 @@
|
||||
/user/storage-key StorageKeyR POST !free
|
||||
|
||||
/exam-office ExamOfficeR !exam-office:
|
||||
/ EOExamsR GET !system-exam-office
|
||||
/ EOExamsR GET POST !system-exam-office
|
||||
/fields EOFieldsR GET POST
|
||||
/users EOUsersR GET POST !system-exam-office
|
||||
/users/invite EOUsersInviteR GET POST !system-exam-office
|
||||
|
||||
@ -257,6 +257,8 @@ upsertCampusUser upsertMode ldapData = do
|
||||
, userDownloadFiles = userDefaultDownloadFiles
|
||||
, userWarningDays = userDefaultWarningDays
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
, userNotificationSettings = def
|
||||
, userLanguages = Nothing
|
||||
, userCsvOptions = def
|
||||
|
||||
@ -216,24 +216,33 @@ embedRenderMessage ''UniWorX ''ExamUserAction id
|
||||
|
||||
data ExamUserActionData = ExamUserMarkSynchronisedData
|
||||
|
||||
newtype ExamUserCsvExportData = ExamUserCsvExportData
|
||||
data ExamUserCsvExportData = ExamUserCsvExportData
|
||||
{ csvEUserMarkSynchronised :: Bool
|
||||
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||
, csvEUserSetLabel :: Bool
|
||||
}
|
||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||
|
||||
|
||||
-- | View a list of all users' grades that the current user has access to
|
||||
getEGradesR, postEGradesR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
|
||||
getEGradesR = postEGradesR
|
||||
postEGradesR tid ssh csh examn = do
|
||||
uid <- requireAuthId
|
||||
Entity uid User{userCsvOptions=csvOpts} <- requireAuth
|
||||
now <- liftIO getCurrentTime
|
||||
((usersResult, examUsersTable), Entity eId Exam{examFinished}) <- runDB $ do
|
||||
exam@(Entity eid Exam{..}) <- fetchExam tid ssh csh examn
|
||||
Course{..} <- getJust examCourse
|
||||
|
||||
isLecturer <- hasReadAccessTo $ CExamR tid ssh csh examn EUsersR
|
||||
isExamOffice <- hasReadAccessTo $ ExamOfficeR EOExamsR
|
||||
userFunctions <- selectList [ UserFunctionUser ==. uid, UserFunctionFunction ==. SchoolExamOffice ] []
|
||||
|
||||
userCsvExportLabel' <- E.select . E.from $ \examOfficeLabel -> do
|
||||
E.where_ $ maybe E.false (\expLbl -> examOfficeLabel E.^. ExamOfficeLabelName E.==. E.val expLbl) (csvExportLabel csvOpts)
|
||||
E.&&. examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
|
||||
return examOfficeLabel
|
||||
let userCsvExportLabel = listToMaybe userCsvExportLabel'
|
||||
|
||||
let
|
||||
participantLink :: (MonadHandler m, HandlerSite m ~ UniWorX) => UserId -> m (SomeRoute UniWorX)
|
||||
participantLink partId = liftHandler $ do
|
||||
@ -332,7 +341,7 @@ postEGradesR tid ssh csh examn = do
|
||||
colSynced = Colonnade.singleton (fromSortable . Sortable (Just "is-synced") $ i18nCell MsgExamUserSynchronised) $ \x -> cell . flip runReaderT x $ do
|
||||
syncs <- asks $ sortOn (Down . view _3) . toListOf resultSynchronised
|
||||
lastChange <- view $ resultExamResult . _entityVal . _examResultLastChanged
|
||||
user <- view $ resultUser . _entityVal
|
||||
User{..} <- view $ resultUser . _entityVal
|
||||
isSynced <- view resultIsSynced
|
||||
let
|
||||
hasSyncs = has folded syncs
|
||||
@ -431,8 +440,17 @@ postEGradesR tid ssh csh examn = do
|
||||
dbtCsvEncode = Just DBTCsvEncode
|
||||
{ dbtCsvExportForm = ExamUserCsvExportData
|
||||
<$> apopt checkBoxField (fslI MsgExamOfficeExamUserMarkSynchronisedCsv & setTooltip MsgExamOfficeExamUserMarkSynchronisedCsvTip) (Just False)
|
||||
<*> bool
|
||||
( pure False )
|
||||
( maybe
|
||||
(aforced checkBoxField (fslI MsgExamOfficeLabelSetLabelOnExport & setTooltip MsgExamOfficeLabelSetLabelOnExportForcedTip) False)
|
||||
(\expLbl -> apopt checkBoxField (fslI MsgExamOfficeLabelSetLabelOnExport & setTooltip (MsgExamOfficeLabelSetLabelOnExportTip expLbl)) (Just True))
|
||||
(examOfficeLabelName . entityVal <$> userCsvExportLabel)
|
||||
)
|
||||
isExamOffice
|
||||
, dbtCsvDoEncode = \ExamUserCsvExportData{..} -> C.mapM $ \(E.Value k, row) -> do
|
||||
when csvEUserMarkSynchronised $ markSynced k
|
||||
when csvEUserSetLabel $ maybe (return ()) (\lbl -> void $ upsert (ExamOfficeExamLabel eid lbl) [ExamOfficeExamLabelLabel =. lbl]) (entityKey <$> userCsvExportLabel)
|
||||
return $ ExamUserTableCsv
|
||||
(row ^. resultUser . _entityVal . _userSurname)
|
||||
(row ^. resultUser . _entityVal . _userFirstName)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
|
||||
|
||||
module Handler.ExamOffice.Exams
|
||||
( getEOExamsR
|
||||
( getEOExamsR, postEOExamsR
|
||||
) where
|
||||
|
||||
import Import
|
||||
@ -16,46 +16,94 @@ import qualified Database.Esqueleto.Utils as E
|
||||
import qualified Colonnade
|
||||
|
||||
import qualified Data.Conduit.Combinators as C
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Set as Set
|
||||
|
||||
|
||||
data ExamAction = ExamSetLabel | ExamRemoveLabel
|
||||
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||
deriving anyclass (Universe, Finite)
|
||||
|
||||
nullaryPathPiece ''ExamAction $ camelToPathPiece' 1
|
||||
embedRenderMessage ''UniWorX ''ExamAction id
|
||||
|
||||
data ExamActionData = ExamSetLabelData
|
||||
{ easlNewLabel :: ExamOfficeLabelId
|
||||
}
|
||||
| ExamRemoveLabelData
|
||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||
|
||||
|
||||
data ExamsTableFilterProj = ExamsTableFilterProj
|
||||
{ etProjFilterMayAccess :: Maybe Bool
|
||||
{ etProjFilterMayAccess :: Maybe Bool
|
||||
, etProjFilterHasResults :: Maybe Bool
|
||||
, etProjFilterIsSynced :: Maybe Bool
|
||||
, etProjFilterLabel :: Maybe (Either ExamOfficeExternalExamLabelId ExamOfficeExamLabelId)
|
||||
, etProjFilterIsSynced :: Maybe Bool
|
||||
}
|
||||
|
||||
instance Default ExamsTableFilterProj where
|
||||
def = ExamsTableFilterProj
|
||||
{ etProjFilterMayAccess = Nothing
|
||||
{ etProjFilterMayAccess = Nothing
|
||||
, etProjFilterHasResults = Nothing
|
||||
, etProjFilterIsSynced = Nothing
|
||||
, etProjFilterLabel = Nothing
|
||||
, etProjFilterIsSynced = Nothing
|
||||
}
|
||||
|
||||
makeLenses_ ''ExamsTableFilterProj
|
||||
|
||||
|
||||
type ExamsTableExpr = ( E.SqlExpr (Maybe (Entity Exam))
|
||||
`E.InnerJoin` E.SqlExpr (Maybe (Entity Course))
|
||||
`E.InnerJoin` E.SqlExpr (Maybe (Entity School))
|
||||
type ExamsTableExpr = ( ( E.SqlExpr (Maybe (Entity Exam ))
|
||||
`E.InnerJoin` E.SqlExpr (Maybe (Entity Course))
|
||||
`E.InnerJoin` E.SqlExpr (Maybe (Entity School))
|
||||
)
|
||||
`E.LeftOuterJoin`
|
||||
( E.SqlExpr (Maybe (Entity ExamOfficeExamLabel))
|
||||
`E.InnerJoin` E.SqlExpr (Maybe (Entity ExamOfficeLabel))
|
||||
)
|
||||
)
|
||||
`E.FullOuterJoin` ( E.SqlExpr (Maybe (Entity ExternalExam))
|
||||
`E.LeftOuterJoin`
|
||||
( E.SqlExpr (Maybe (Entity ExamOfficeExternalExamLabel))
|
||||
`E.InnerJoin` E.SqlExpr (Maybe (Entity ExamOfficeLabel))
|
||||
)
|
||||
)
|
||||
`E.FullOuterJoin` E.SqlExpr (Maybe (Entity ExternalExam))
|
||||
|
||||
type ExamsTableData = DBRow ( Either (Entity ExternalExam) (Entity Exam, Entity Course, Entity School)
|
||||
, Natural, Natural
|
||||
type ExamsTableData = DBRow ( Either
|
||||
( Entity ExternalExam
|
||||
, Maybe (Entity ExamOfficeLabel)
|
||||
)
|
||||
( Entity Exam
|
||||
, Entity Course
|
||||
, Entity School
|
||||
, Maybe (Entity ExamOfficeLabel)
|
||||
)
|
||||
, Maybe Natural
|
||||
, Maybe Natural
|
||||
)
|
||||
|
||||
queryExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity Exam)))
|
||||
queryExam = to $ $(E.sqlIJproj 3 1) . $(E.sqlFOJproj 2 1)
|
||||
queryExam = to $ $(E.sqlIJproj 3 1) . $(E.sqlLOJproj 2 1) . $(E.sqlFOJproj 2 1)
|
||||
|
||||
queryCourse :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity Course)))
|
||||
queryCourse = to $ $(E.sqlIJproj 3 2) . $(E.sqlFOJproj 2 1)
|
||||
queryCourse = to $ $(E.sqlIJproj 3 2) . $(E.sqlLOJproj 2 1) . $(E.sqlFOJproj 2 1)
|
||||
|
||||
querySchool :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity School)))
|
||||
querySchool = to $ $(E.sqlIJproj 3 3) . $(E.sqlFOJproj 2 1)
|
||||
querySchool = to $ $(E.sqlIJproj 3 3) . $(E.sqlLOJproj 2 1) . $(E.sqlFOJproj 2 1)
|
||||
|
||||
queryExamLabel :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeExamLabel)))
|
||||
queryExamLabel = to $ $(E.sqlIJproj 2 1) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 1)
|
||||
|
||||
queryLabelExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeLabel)))
|
||||
queryLabelExam = to $ $(E.sqlIJproj 2 2) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 1)
|
||||
|
||||
queryExternalExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExternalExam)))
|
||||
queryExternalExam = to $(E.sqlFOJproj 2 2)
|
||||
queryExternalExam = to $ $(E.sqlLOJproj 2 1) . $(E.sqlFOJproj 2 2)
|
||||
|
||||
queryExternalExamLabel :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeExternalExamLabel)))
|
||||
queryExternalExamLabel = to $ $(E.sqlIJproj 2 1) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 2)
|
||||
|
||||
queryLabelExternalExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeLabel)))
|
||||
queryLabelExternalExam = to $ $(E.sqlIJproj 2 2) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 2)
|
||||
|
||||
resultExam :: Traversal' ExamsTableData (Entity Exam)
|
||||
resultExam = _dbrOutput . _1 . _Right . _1
|
||||
@ -67,9 +115,12 @@ resultSchool :: Traversal' ExamsTableData (Entity School)
|
||||
resultSchool = _dbrOutput . _1 . _Right . _3
|
||||
|
||||
resultExternalExam :: Traversal' ExamsTableData (Entity ExternalExam)
|
||||
resultExternalExam = _dbrOutput . _1 . _Left
|
||||
resultExternalExam = _dbrOutput . _1 . _Left . _1
|
||||
|
||||
resultSynchronised, resultResults :: Lens' ExamsTableData Natural
|
||||
resultLabel :: Traversal' ExamsTableData (Maybe (Entity ExamOfficeLabel))
|
||||
resultLabel = _dbrOutput . _1 . choosing _2 _4
|
||||
|
||||
resultSynchronised, resultResults :: Lens' ExamsTableData (Maybe Natural)
|
||||
resultSynchronised = _dbrOutput . _2
|
||||
resultResults = _dbrOutput . _3
|
||||
|
||||
@ -77,14 +128,45 @@ resultIsSynced :: Getter ExamsTableData Bool
|
||||
resultIsSynced = to $ (>=) <$> view resultSynchronised <*> view resultResults
|
||||
|
||||
|
||||
-- | List of all exams where the current user may (in her function as
|
||||
-- exam-office) access users grades
|
||||
getEOExamsR :: Handler Html
|
||||
getEOExamsR = do
|
||||
uid <- requireAuthId
|
||||
-- | List of all exams where the current user may (in their function as exam-office) access users grades
|
||||
getEOExamsR, postEOExamsR :: Handler Html
|
||||
getEOExamsR = postEOExamsR
|
||||
postEOExamsR = do
|
||||
(uid, User{..}) <- requireAuthPair
|
||||
now <- liftIO getCurrentTime
|
||||
mr <- getMessageRender
|
||||
|
||||
getSynced <- lookupGetParam "synced" <&>
|
||||
(\case
|
||||
Just "yes" -> True
|
||||
Just "no" -> False
|
||||
_ -> userExamOfficeGetSynced
|
||||
)
|
||||
|
||||
getLabels <- lookupGetParam "labels" <&>
|
||||
(\case
|
||||
Just "yes" -> True
|
||||
Just "no" -> False
|
||||
_ -> userExamOfficeGetLabels
|
||||
)
|
||||
|
||||
(examsRes, examsTable) <- runDB $ do
|
||||
let labelFilterNoLabelOption = Option
|
||||
{ optionDisplay = mr MsgExamOfficeExamsNoLabel
|
||||
, optionInternalValue = Nothing
|
||||
, optionExternalValue = "no-label"
|
||||
}
|
||||
labelFilterOptions <- mkOptionList . (labelFilterNoLabelOption :) <$> do
|
||||
labels <- E.select . E.from $ \examOfficeLabel -> do
|
||||
E.where_ $ examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
|
||||
E.orderBy [ E.asc $ examOfficeLabel E.^. ExamOfficeLabelName ]
|
||||
return examOfficeLabel
|
||||
return . flip map labels $ \(Entity lblId ExamOfficeLabel{..})
|
||||
-> Option { optionDisplay = examOfficeLabelName
|
||||
, optionInternalValue = Just lblId
|
||||
, optionExternalValue = examOfficeLabelName
|
||||
}
|
||||
|
||||
examsTable <- runDB $ do
|
||||
let
|
||||
examLink :: Course -> Exam -> SomeRoute UniWorX
|
||||
examLink Course{..} Exam{..}
|
||||
@ -98,43 +180,63 @@ getEOExamsR = do
|
||||
externalExamLink ExternalExam{..}
|
||||
= SomeRoute $ EExamR externalExamTerm externalExamSchool externalExamCourseName externalExamExamName EEGradesR
|
||||
|
||||
examActions :: Map ExamAction (AForm Handler ExamActionData)
|
||||
examActions = Map.fromList $
|
||||
bool mempty
|
||||
[ ( ExamSetLabel, ExamSetLabelData
|
||||
<$> apopt (selectField' Nothing . fmap (fmap entityKey) $ optionsPersist [ExamOfficeLabelUser ==. uid] [Asc ExamOfficeLabelName] examOfficeLabelName) (fslI MsgExamLabel) Nothing
|
||||
)
|
||||
, ( ExamRemoveLabel, pure ExamRemoveLabelData )
|
||||
] getLabels
|
||||
|
||||
examsDBTable = DBTable{..}
|
||||
where
|
||||
dbtSQLQuery = runReaderT $ do
|
||||
exam <- view queryExam
|
||||
course <- view queryCourse
|
||||
school <- view querySchool
|
||||
externalExam <- view queryExternalExam
|
||||
exam <- view queryExam
|
||||
course <- view queryCourse
|
||||
school <- view querySchool
|
||||
mExamLabel <- view queryExamLabel
|
||||
mLabelExam <- view queryLabelExam
|
||||
externalExam <- view queryExternalExam
|
||||
mExternalExamLabel <- view queryExternalExamLabel
|
||||
mLabelExternalExam <- view queryLabelExternalExam
|
||||
|
||||
lift $ do
|
||||
E.on $ mExternalExamLabel E.?. ExamOfficeExternalExamLabelLabel E.==. mLabelExternalExam E.?. ExamOfficeLabelId
|
||||
E.on $ E.maybe E.true (\externalExamLabelExternalExamId ->
|
||||
externalExam E.?. ExternalExamId E.==. E.just externalExamLabelExternalExamId
|
||||
) (mExternalExamLabel E.?. ExamOfficeExternalExamLabelExternalExam)
|
||||
E.on E.false
|
||||
E.on $ school E.?. SchoolId E.==. course E.?. CourseSchool
|
||||
E.on $ exam E.?. ExamCourse E.==. course E.?. CourseId
|
||||
E.on $ mExamLabel E.?. ExamOfficeExamLabelLabel E.==. mLabelExam E.?. ExamOfficeLabelId
|
||||
E.on $ E.maybe E.true (\examLabelExamId ->
|
||||
exam E.?. ExamId E.==. E.just examLabelExamId
|
||||
) (mExamLabel E.?. ExamOfficeExamLabelExam)
|
||||
E.on $ course E.?. CourseSchool E.==. school E.?. SchoolId
|
||||
E.on $ exam E.?. ExamCourse E.==. course E.?. CourseId
|
||||
|
||||
E.where_ $ (E.not_ (E.isNothing $ exam E.?. ExamId) E.&&. E.not_ (E.isNothing $ course E.?. CourseId) E.&&. E.isNothing (externalExam E.?. ExternalExamId))
|
||||
E.||. ( E.isNothing (exam E.?. ExamId) E.&&. E.isNothing (course E.?. CourseId) E.&&. E.not_ (E.isNothing $ externalExam E.?. ExternalExamId))
|
||||
E.where_ $ E.val (not getLabels) E.||. (
|
||||
E.val getLabels
|
||||
E.&&. E.maybe E.true (\labelExamUser ->
|
||||
labelExamUser E.==. E.val uid
|
||||
) (mLabelExam E.?. ExamOfficeLabelUser)
|
||||
E.&&. E.maybe E.true (\labelExternalExamUser ->
|
||||
labelExternalExamUser E.==. E.val uid
|
||||
) (mLabelExternalExam E.?. ExamOfficeLabelUser)
|
||||
)
|
||||
|
||||
return (exam, course, school, externalExam)
|
||||
return (exam, course, school, mLabelExam, externalExam, mLabelExternalExam)
|
||||
dbtRowKey = views ($(multifocusG 2) queryExam queryExternalExam) (bimap (E.?. ExamId) (E.?. ExternalExamId))
|
||||
|
||||
-- [ singletonMap "may-access" . FilterProjected $ \(Any b) r -> (== b) <$> if
|
||||
-- | Just exam <- r ^? resultExam . _entityVal
|
||||
-- , Just course <- r ^? resultCourse . _entityVal
|
||||
-- -> hasReadAccessTo . urlRoute $ examLink course exam
|
||||
-- | Just eexam <- r ^? resultExternalExam . _entityVal
|
||||
-- -> hasReadAccessTo . urlRoute $ externalExamLink eexam :: DB Bool
|
||||
-- | otherwise
|
||||
-- -> return $ error "Got neither exam nor externalExam in result"
|
||||
-- , singletonMap "has-results" . FilterProjected $ \(Any b) r -> (return $ b == (r ^. resultResults > 0) :: DB Bool)
|
||||
-- , singletonMap "is-synced" . FilterProjected $ \(Any b) r -> (return $ b == (r ^. resultSynchronised >= r ^. resultResults) :: DB Bool)
|
||||
-- ]
|
||||
|
||||
dbtProj :: _ ExamsTableData
|
||||
dbtProj = (views _dbtProjRow . set _dbrOutput) =<< do -- dbtProjSimple . runReaderT $ do
|
||||
exam <- view $ _dbtProjRow . _dbrOutput . _1
|
||||
course <- view $ _dbtProjRow . _dbrOutput . _2
|
||||
school <- view $ _dbtProjRow . _dbrOutput . _3
|
||||
externalExam <- view $ _dbtProjRow . _dbrOutput . _4
|
||||
dbtProj = (views _dbtProjRow . set _dbrOutput) =<< do
|
||||
exam <- view $ _dbtProjRow . _dbrOutput . _1
|
||||
course <- view $ _dbtProjRow . _dbrOutput . _2
|
||||
school <- view $ _dbtProjRow . _dbrOutput . _3
|
||||
mExamLabel <- view $ _dbtProjRow . _dbrOutput . _4
|
||||
externalExam <- view $ _dbtProjRow . _dbrOutput . _5
|
||||
mExternalExamLabel <- view $ _dbtProjRow . _dbrOutput . _6
|
||||
|
||||
forMM_ (view $ _dbtProjFilter . _etProjFilterMayAccess) $ \b -> if
|
||||
| Just (Entity _ exam') <- exam
|
||||
@ -156,24 +258,41 @@ getEOExamsR = do
|
||||
return $ ExternalExam.resultIsSynced (E.val uid) externalExamResult
|
||||
getResults = getExamResults >> getExternalExamResults
|
||||
foldResult (E.Value isSynced) = (Sum 1, guardMonoid isSynced $ Sum 1)
|
||||
(Sum resultCount, Sum syncedCount) <- lift . lift . runConduit $ getResults .| C.foldMap foldResult
|
||||
|
||||
mCounts <- if getSynced
|
||||
then do
|
||||
(Sum resCount, Sum synCount) <- lift . lift . runConduit $ getResults .| C.foldMap foldResult
|
||||
forMM_ (view $ _dbtProjFilter . _etProjFilterHasResults) $ \b ->
|
||||
guard $ b == (resCount > 0)
|
||||
forMM_ (view $ _dbtProjFilter . _etProjFilterIsSynced) $ \b ->
|
||||
guard $ b == (synCount >= resCount)
|
||||
return $ Just (resCount, synCount)
|
||||
else do
|
||||
forMM_ (view $ _dbtProjFilter . _etProjFilterHasResults) guard
|
||||
return Nothing
|
||||
|
||||
forMM_ (view $ _dbtProjFilter . _etProjFilterHasResults) $ \b ->
|
||||
guard $ b == (resultCount > 0)
|
||||
forMM_ (view $ _dbtProjFilter . _etProjFilterIsSynced) $ \b ->
|
||||
guard $ b == (syncedCount >= resultCount)
|
||||
|
||||
case (exam, course, school, externalExam) of
|
||||
(Just exam', Just course', Just school', Nothing) -> return
|
||||
(Right (exam', course', school'), syncedCount, resultCount)
|
||||
(Nothing, Nothing, Nothing, Just externalExam') -> return
|
||||
(Left externalExam', syncedCount, resultCount)
|
||||
case (exam, course, school, mExamLabel, externalExam, mExternalExamLabel) of
|
||||
(Just exam', Just course', Just school', mExamLabel', Nothing, Nothing) -> return
|
||||
(Right (exam', course', school', mExamLabel'), snd <$> mCounts, fst <$> mCounts)
|
||||
(Nothing, Nothing, Nothing, Nothing, Just externalExam', mExternalExamLabel') -> return
|
||||
(Left (externalExam', mExternalExamLabel'), snd <$> mCounts, fst <$> mCounts)
|
||||
_other -> return $ error "Got exam & externalExam in same result"
|
||||
|
||||
|
||||
colLabel = Colonnade.singleton (fromSortable . Sortable (Just "label") $ i18nCell MsgTableExamLabel) $ \x -> flip runReader x $ do
|
||||
mLabel <- preview resultLabel
|
||||
|
||||
-- TODO: use select frontend util
|
||||
if
|
||||
| Just (Just (Entity _ ExamOfficeLabel{..})) <- mLabel
|
||||
-> return $ cell $(widgetFile "widgets/exam-office-label")
|
||||
| otherwise -> return $ cell mempty
|
||||
|
||||
colSynced = Colonnade.singleton (fromSortable . Sortable (Just "synced") $ i18nCell MsgExamSynchronised) $ \x -> flip runReader x $ do
|
||||
mExam <- preview resultExam
|
||||
mSchool <- preview resultSchool
|
||||
mExam <- preview resultExam
|
||||
mSchool <- preview resultSchool
|
||||
mSynced <- view resultSynchronised
|
||||
mResults <- view resultResults
|
||||
|
||||
if
|
||||
| Just (Entity _ Exam{examClosed, examFinished}) <- mExam
|
||||
@ -182,12 +301,10 @@ getEOExamsR = do
|
||||
(NTop examClosed > NTop (Just now))
|
||||
$ is _ExamCloseSeparate schoolExamCloseMode
|
||||
-> return . cell $ toWidget iconNew
|
||||
| otherwise
|
||||
| Just synced <- mSynced
|
||||
, Just results <- mResults
|
||||
-> do
|
||||
synced <- view resultSynchronised
|
||||
results <- view resultResults
|
||||
isSynced <- view resultIsSynced
|
||||
|
||||
return $ cell
|
||||
[whamlet|
|
||||
$newline never
|
||||
@ -199,11 +316,14 @@ getEOExamsR = do
|
||||
& cellAttrs <>~ [ ("class", "heated")
|
||||
, ("style", [st|--hotness: #{tshow (heat results synced)}|])
|
||||
]
|
||||
| otherwise -> return $ cell mempty
|
||||
|
||||
|
||||
dbtColonnade :: Colonnade Sortable _ _
|
||||
dbtColonnade = mconcat
|
||||
[ colSynced
|
||||
[ bool mempty (dbSelect (applying _2) id $ \DBRow{ dbrOutput=(ex,_,_) } -> return $ bimap (\(Entity eeId _,_) -> eeId) (\(Entity eId _,_,_,_) -> eId) ex) (not $ Map.null examActions)
|
||||
, bool mempty colLabel getLabels
|
||||
, bool mempty colSynced getSynced
|
||||
, maybeAnchorColonnade ( runMaybeT $ mpreview ($(multifocusG 2) (pre $ resultCourse . _entityVal) (pre $ resultExam . _entityVal) . to (uncurry $ liftA2 examLink) . _Just)
|
||||
<|> mpreviews (resultExternalExam . _entityVal) externalExamLink
|
||||
)
|
||||
@ -216,12 +336,20 @@ getEOExamsR = do
|
||||
, emptyOpticColonnade (resultCourse . _entityVal . _courseSchool <> resultExternalExam . _entityVal . _externalExamSchool) colSchool
|
||||
, emptyOpticColonnade (resultCourse . _entityVal . _courseTerm <> resultExternalExam . _entityVal . _externalExamTerm) colTermShort
|
||||
]
|
||||
dbtSorting = mconcat
|
||||
dbtSorting = mconcat $
|
||||
bool mempty
|
||||
[ singletonMap "label-prio" $
|
||||
SortProjected . comparing $ (fmap . fmap $ examOfficeLabelPriority . entityVal) <$> preview resultLabel
|
||||
, singletonMap "label-status" $
|
||||
SortProjected . comparing $ (fmap . fmap $ examOfficeLabelStatus . entityVal) <$> preview resultLabel
|
||||
] getLabels <>
|
||||
bool mempty
|
||||
[ singletonMap "synced" $
|
||||
SortProjected . comparing $ ((/) `on` toRational) <$> view resultSynchronised <*> view resultResults
|
||||
SortProjected . comparing $ ((/) `on` toRational . fromMaybe 0) <$> view resultSynchronised <*> view resultResults
|
||||
, singletonMap "is-synced" $
|
||||
SortProjected . comparing $ (>=) <$> view resultSynchronised <*> view resultResults
|
||||
, sortExamName (to $ E.unsafeCoalesce . sequence [views queryExam (E.?. ExamName), views queryExternalExam (E.?. ExternalExamExamName)])
|
||||
] getSynced <>
|
||||
[ sortExamName (to $ E.unsafeCoalesce . sequence [views queryExam (E.?. ExamName), views queryExternalExam (E.?. ExternalExamExamName)])
|
||||
, sortExamTime (queryExam . $(multifocusG 2) (to $ E.joinV . (E.?. ExamStart)) (to $ E.joinV . (E.?. ExamEnd)))
|
||||
, sortExamFinished (queryExam . to (E.joinV . (E.?. ExamFinished)))
|
||||
, sortExamClosed (queryExam . to (E.joinV . (E.?. ExamClosed)))
|
||||
@ -231,31 +359,67 @@ getEOExamsR = do
|
||||
]
|
||||
|
||||
dbtFilter = mconcat
|
||||
[ singletonMap "may-access" . FilterProjected $ (_etProjFilterMayAccess ?~) . getAny
|
||||
[ singletonMap "may-access" . FilterProjected $ (_etProjFilterMayAccess ?~) . getAny
|
||||
, singletonMap "has-results" . FilterProjected $ (_etProjFilterHasResults ?~) . getAny
|
||||
, singletonMap "is-synced" . FilterProjected $ (_etProjFilterIsSynced ?~) . getAny
|
||||
]
|
||||
dbtFilterUI = mconcat
|
||||
[ flip (prismAForm $ singletonFilter "is-synced" . maybePrism _PathPiece) $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamSynchronised)
|
||||
, singletonMap "is-synced" . FilterProjected $ (_etProjFilterIsSynced ?~) . getAny
|
||||
, singletonMap "label" . FilterColumn . E.mkExactFilter $ views queryLabelExam (E.?. ExamOfficeLabelId)
|
||||
]
|
||||
dbtFilterUI mPrev = mconcat $
|
||||
[ prismAForm (singletonFilter "label" . maybePrism _PathPiece) mPrev $ aopt (selectField' (Just $ SomeMessage MsgTableNoFilter) $ return labelFilterOptions) (fslI MsgExamLabel)
|
||||
| getLabels ] <>
|
||||
[ prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamSynchronised)
|
||||
| getSynced ]
|
||||
|
||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||
dbtParams = def
|
||||
|
||||
dbtParams = DBParamsForm
|
||||
{ dbParamsFormMethod = POST
|
||||
, dbParamsFormAction = Just . SomeRoute $ ExamOfficeR EOExamsR
|
||||
, dbParamsFormAttrs = []
|
||||
, dbParamsFormSubmit = FormSubmit
|
||||
, dbParamsFormAdditional
|
||||
= renderAForm FormStandard
|
||||
$ (, mempty) . First . Just
|
||||
<$> multiActionA examActions (fslI MsgTableAction) Nothing
|
||||
, dbParamsFormEvaluate = liftHandler . runFormPost
|
||||
, dbParamsFormResult = id
|
||||
, dbParamsFormIdent = def
|
||||
}
|
||||
|
||||
dbtIdent :: Text
|
||||
dbtIdent = "exams"
|
||||
|
||||
dbtCsvEncode = noCsvEncode
|
||||
dbtCsvDecode = Nothing
|
||||
|
||||
|
||||
dbtExtraReps = []
|
||||
|
||||
examsDBTableValidator = def
|
||||
& defaultSorting [SortAscBy "is-synced", SortAscBy "exam-time"]
|
||||
& defaultSorting (bool mempty [SortDescBy "label-prio", SortAscBy "label-status"] getLabels <> bool mempty [SortAscBy "is-synced"] getSynced <> [SortAscBy "exam-time"])
|
||||
& forceFilter "may-access" (Any True)
|
||||
& forceFilter "has-results" (Any True)
|
||||
|
||||
dbTableWidget' examsDBTableValidator examsDBTable
|
||||
postprocess :: FormResult (First ExamActionData , DBFormResult (Either ExternalExamId ExamId) Bool (DBRow (Either (Entity ExternalExam, Maybe (Entity ExamOfficeLabel)) (Entity Exam, Entity Course, Entity School, Maybe (Entity ExamOfficeLabel)), Maybe Natural, Maybe Natural)))
|
||||
-> FormResult ( ExamActionData , Set (Either ExternalExamId ExamId))
|
||||
postprocess (FormFailure errs) = FormFailure errs
|
||||
postprocess FormMissing = FormMissing
|
||||
postprocess (FormSuccess (First mExamActionData, examRes))
|
||||
| Just act <- mExamActionData = FormSuccess . (act,) . Map.keysSet . Map.filter id $ getDBFormResult (const False) examRes
|
||||
| otherwise = FormMissing
|
||||
|
||||
over _1 postprocess <$> dbTable examsDBTableValidator examsDBTable
|
||||
|
||||
formResult examsRes $ \(examAction, exams) -> case examAction of
|
||||
ExamSetLabelData{..} -> do
|
||||
runDB . forM_ (Set.toList exams) $ either (\eeid -> void $ upsert (ExamOfficeExternalExamLabel eeid easlNewLabel) [ExamOfficeExternalExamLabelLabel =. easlNewLabel]) (\eid -> void $ upsert (ExamOfficeExamLabel eid easlNewLabel) [ExamOfficeExamLabelLabel =. easlNewLabel])
|
||||
addMessageI Success $ MsgExamLabelsSet (Set.size exams)
|
||||
redirect $ ExamOfficeR EOExamsR
|
||||
ExamRemoveLabelData -> do
|
||||
runDB . forM_ (Set.toList exams) $ either
|
||||
(\eeId -> E.delete . E.from $ \extExLabel -> E.where_ (extExLabel E.^. ExamOfficeExternalExamLabelExternalExam E.==. E.val eeId))
|
||||
(\eId -> E.delete . E.from $ \exLabel -> E.where_ (exLabel E.^. ExamOfficeExamLabelExam E.==. E.val eId))
|
||||
addMessageI Success $ MsgExamLabelsRemoved (Set.size exams)
|
||||
redirect $ ExamOfficeR EOExamsR
|
||||
|
||||
siteLayoutMsg MsgHeadingExamList $ do
|
||||
setTitleI MsgHeadingExamList
|
||||
|
||||
@ -31,6 +31,20 @@ import Jobs
|
||||
import Foundation.Yesod.Auth (updateUserLanguage)
|
||||
|
||||
|
||||
data ExamOfficeSettings
|
||||
= ExamOfficeSettings
|
||||
{ eosettingsGetSynced :: Bool
|
||||
, eosettingsGetLabels :: Bool
|
||||
, eosettingsLabels :: EOLabels
|
||||
}
|
||||
|
||||
type EOLabelData
|
||||
= ( ExamOfficeLabelName
|
||||
, MessageStatus -- status
|
||||
, Int -- priority; also used for label ordering
|
||||
)
|
||||
type EOLabels = Map (Either ExamOfficeLabelName ExamOfficeLabelId) EOLabelData
|
||||
|
||||
data SettingsForm = SettingsForm
|
||||
{ stgDisplayName :: UserDisplayName
|
||||
, stgDisplayEmail :: UserEmail
|
||||
@ -43,6 +57,7 @@ data SettingsForm = SettingsForm
|
||||
, stgDownloadFiles :: Bool
|
||||
, stgWarningDays :: NominalDiffTime
|
||||
, stgShowSex :: Bool
|
||||
, stgExamOfficeSettings :: ExamOfficeSettings
|
||||
, stgSchools :: Set SchoolId
|
||||
, stgNotificationSettings :: NotificationSettings
|
||||
, stgAllocationNotificationSettings :: Map AllocationId (Maybe Bool)
|
||||
@ -115,6 +130,7 @@ makeSettingForm template html = do
|
||||
& setTooltip MsgWarningDaysTip
|
||||
) (stgWarningDays <$> template)
|
||||
<*> apopt checkBoxField (fslI MsgShowSex & setTooltip MsgShowSexTip) (stgShowSex <$> template)
|
||||
<*> examOfficeForm (stgExamOfficeSettings <$> template)
|
||||
<* aformSection MsgFormNotifications
|
||||
<*> schoolsForm (stgSchools <$> template)
|
||||
<*> notificationForm (stgNotificationSettings <$> template)
|
||||
@ -311,6 +327,101 @@ allocationNotificationForm = maybe (pure mempty) allocationNotificationForm' . (
|
||||
where funcForm' forms = funcForm forms (fslI MsgFormAllocationNotifications & setTooltip MsgFormAllocationNotificationsTip) False
|
||||
|
||||
|
||||
examOfficeForm :: Maybe ExamOfficeSettings -> AForm Handler ExamOfficeSettings
|
||||
examOfficeForm template = wFormToAForm $ do
|
||||
(_uid, User{userExamOfficeGetSynced,userExamOfficeGetLabels}) <- requireAuthPair
|
||||
currentRoute <- fromMaybe (error "examOfficeForm called from 404-handler") <$> liftHandler getCurrentRoute
|
||||
mr <- getMessageRender
|
||||
|
||||
let
|
||||
userExamOfficeLabels :: EOLabels
|
||||
userExamOfficeLabels = maybe mempty eosettingsLabels template
|
||||
|
||||
eoLabelsForm :: AForm Handler EOLabels
|
||||
eoLabelsForm = wFormToAForm $ do
|
||||
let
|
||||
miAdd :: ListPosition
|
||||
-> Natural
|
||||
-> ListLength
|
||||
-> (Text -> Text)
|
||||
-> FieldView UniWorX
|
||||
-> Maybe
|
||||
(Form (Map ListPosition (Either ExamOfficeLabelName ExamOfficeLabelId)
|
||||
-> FormResult (Map ListPosition (Either ExamOfficeLabelName ExamOfficeLabelId)))
|
||||
)
|
||||
miAdd _ _ _ nudge submitView = Just $ \csrf -> do
|
||||
(addRes, addView) <- mpreq textField (fslI MsgExamOfficeLabelName & addName (nudge "name")) Nothing
|
||||
let
|
||||
addRes' = addRes <&> \nLabel oldData@(maybe 0 (succ . fst) . Map.lookupMax -> kStart) -> if
|
||||
| Set.member (Left nLabel) . Set.fromList $ Map.elems oldData
|
||||
-> FormFailure [mr MsgExamOfficeLabelAlreadyExists]
|
||||
| otherwise
|
||||
-> FormSuccess $ Map.singleton kStart (Left nLabel)
|
||||
return (addRes', $(widgetFile "profile/exam-office-labels/add"))
|
||||
|
||||
miCell :: ListPosition
|
||||
-> Either ExamOfficeLabelName ExamOfficeLabelId
|
||||
-> Maybe EOLabelData
|
||||
-> (Text -> Text)
|
||||
-> Form EOLabelData
|
||||
miCell _ eoLabel initRes nudge csrf = do
|
||||
labelIdent <- case eoLabel of
|
||||
Left lblName -> return lblName
|
||||
Right lblId -> do
|
||||
ExamOfficeLabel{examOfficeLabelName} <- liftHandler . runDB $ getJust lblId
|
||||
return examOfficeLabelName
|
||||
(statusRes, statusView) <- mreq (selectField optionsFinite) (fslI MsgExamOfficeLabelStatus & addName (nudge "status")) ((\(_,x,_) -> x) <$> initRes)
|
||||
(priorityRes, priorityView) <- mreq intField (fslI MsgExamOfficeLabelPriority & addName (nudge "priority")) (((\(_,_,x) -> x) <$> initRes) <|> Just 0)
|
||||
let
|
||||
res :: FormResult EOLabelData
|
||||
res = (,,) <$> FormSuccess labelIdent <*> statusRes <*> priorityRes
|
||||
return (res, $(widgetFile "profile/exam-office-labels/cell"))
|
||||
|
||||
miDelete :: Map ListPosition (Either ExamOfficeLabelName ExamOfficeLabelId)
|
||||
-> ListPosition
|
||||
-> MaybeT (MForm Handler) (Map ListPosition ListPosition)
|
||||
miDelete = miDeleteList
|
||||
|
||||
miAddEmpty :: ListPosition
|
||||
-> Natural
|
||||
-> ListLength
|
||||
-> Set ListPosition
|
||||
miAddEmpty _ _ _ = Set.empty
|
||||
|
||||
miButtonAction :: forall p.
|
||||
PathPiece p
|
||||
=> p
|
||||
-> Maybe (SomeRoute UniWorX)
|
||||
miButtonAction frag = Just . SomeRoute $ currentRoute :#: frag
|
||||
|
||||
miLayout :: ListLength
|
||||
-> Map ListPosition (Either ExamOfficeLabelName ExamOfficeLabelId, FormResult EOLabelData)
|
||||
-> Map ListPosition Widget
|
||||
-> Map ListPosition (FieldView UniWorX)
|
||||
-> Map (Natural, ListPosition) Widget
|
||||
-> Widget
|
||||
miLayout lLength _ cellWdgts delButtons addWdgets = $(widgetFile "profile/exam-office-labels/layout")
|
||||
|
||||
miIdent :: Text
|
||||
miIdent = "exam-office-labels"
|
||||
|
||||
filledData :: Maybe (Map ListPosition (Either ExamOfficeLabelName ExamOfficeLabelId, EOLabelData))
|
||||
filledData = Just . Map.fromList . zip [0..] $ Map.toList userExamOfficeLabels
|
||||
|
||||
fmap (Map.fromList . Map.elems) <$> massInputW MassInput{..} (fslI MsgExamOfficeLabels & setTooltip MsgExamOfficeLabelsTip) False filledData
|
||||
|
||||
userIsExamOffice <- liftHandler . hasReadAccessTo $ ExamOfficeR EOExamsR
|
||||
if userIsExamOffice
|
||||
then
|
||||
aFormToWForm $ ExamOfficeSettings
|
||||
<$ aformSection MsgFormExamOffice
|
||||
<*> apopt checkBoxField (fslI MsgExamOfficeGetSynced & setTooltip MsgExamOfficeGetSyncedTip) (eosettingsGetSynced <$> template)
|
||||
<*> apopt checkBoxField (fslI MsgExamOfficeGetLabels & setTooltip MsgExamOfficeGetLabelsTip) (eosettingsGetLabels <$> template)
|
||||
<*> eoLabelsForm
|
||||
else
|
||||
return . pure . fromMaybe (ExamOfficeSettings userExamOfficeGetSynced userExamOfficeGetLabels userExamOfficeLabels) $ template
|
||||
|
||||
|
||||
validateSettings :: User -> FormValidator SettingsForm Handler ()
|
||||
validateSettings User{..} = do
|
||||
userDisplayName' <- use _stgDisplayName
|
||||
@ -342,12 +453,15 @@ getProfileR, postProfileR :: Handler Html
|
||||
getProfileR = postProfileR
|
||||
postProfileR = do
|
||||
(uid, user@User{..}) <- requireAuthPair
|
||||
userSchools <- fmap (setOf $ folded . _Value) . runDB . E.select . E.from $ \school -> do
|
||||
E.where_ . E.exists . E.from $ \userSchool ->
|
||||
E.where_ $ E.not_ (userSchool E.^. UserSchoolIsOptOut)
|
||||
E.&&. userSchool E.^. UserSchoolUser E.==. E.val uid
|
||||
E.&&. userSchool E.^. UserSchoolSchool E.==. school E.^. SchoolId
|
||||
return $ school E.^. SchoolId
|
||||
(userSchools, userExamOfficeLabels) <- runDB $ do
|
||||
userSchools <- fmap (setOf $ folded . _Value) . E.select . E.from $ \school -> do
|
||||
E.where_ . E.exists . E.from $ \userSchool ->
|
||||
E.where_ $ E.not_ (userSchool E.^. UserSchoolIsOptOut)
|
||||
E.&&. userSchool E.^. UserSchoolUser E.==. E.val uid
|
||||
E.&&. userSchool E.^. UserSchoolSchool E.==. school E.^. SchoolId
|
||||
return $ school E.^. SchoolId
|
||||
userExamOfficeLabels <- selectList [ ExamOfficeLabelUser ==. uid ] []
|
||||
return (userSchools, userExamOfficeLabels)
|
||||
allocs <- runDB $ getAllocationNotifications uid
|
||||
let settingsTemplate = Just SettingsForm
|
||||
{ stgDisplayName = userDisplayName
|
||||
@ -363,6 +477,11 @@ postProfileR = do
|
||||
, stgNotificationSettings = userNotificationSettings
|
||||
, stgWarningDays = userWarningDays
|
||||
, stgShowSex = userShowSex
|
||||
, stgExamOfficeSettings = ExamOfficeSettings
|
||||
{ eosettingsGetSynced = userExamOfficeGetSynced
|
||||
, eosettingsGetLabels = userExamOfficeGetLabels
|
||||
, eosettingsLabels = flip foldMap userExamOfficeLabels $ \(Entity eolid ExamOfficeLabel{..}) -> Map.singleton (Right eolid) (examOfficeLabelName,examOfficeLabelStatus,examOfficeLabelPriority)
|
||||
}
|
||||
, stgAllocationNotificationSettings = allocs
|
||||
}
|
||||
((res,formWidget), formEnctype) <- runFormPost . validateForm (validateSettings user) . identifyForm ProfileSettings $ makeSettingForm settingsTemplate
|
||||
@ -381,6 +500,8 @@ postProfileR = do
|
||||
, UserWarningDays =. stgWarningDays
|
||||
, UserNotificationSettings =. stgNotificationSettings
|
||||
, UserShowSex =. stgShowSex
|
||||
, UserExamOfficeGetSynced =. (stgExamOfficeSettings & eosettingsGetSynced)
|
||||
, UserExamOfficeGetLabels =. (stgExamOfficeSettings & eosettingsGetLabels)
|
||||
] ++ [ UserDisplayEmail =. stgDisplayEmail | userDisplayEmail == stgDisplayEmail ]
|
||||
setAllocationNotifications uid stgAllocationNotificationSettings
|
||||
updateFavourites Nothing
|
||||
@ -406,6 +527,26 @@ postProfileR = do
|
||||
}
|
||||
[ UserSchoolIsOptOut =. True
|
||||
]
|
||||
let
|
||||
oldExamLabels = userExamOfficeLabels
|
||||
newExamLabels = stgExamOfficeSettings & eosettingsLabels
|
||||
forM_ oldExamLabels $ \(Entity eolid ExamOfficeLabel{..}) -> unless (Right eolid `Map.member` newExamLabels || Left examOfficeLabelName `Map.member` newExamLabels) $ do
|
||||
E.delete . E.from $ \examOfficeExternalExamLabel -> E.where_ $ examOfficeExternalExamLabel E.^. ExamOfficeExternalExamLabelLabel E.==. E.val eolid
|
||||
E.delete . E.from $ \examOfficeExamLabel -> E.where_ $ examOfficeExamLabel E.^. ExamOfficeExamLabelLabel E.==. E.val eolid
|
||||
when (csvExportLabel userCsvOptions == Just examOfficeLabelName) $
|
||||
update uid [ UserCsvOptions =. userCsvOptions { csvExportLabel = Nothing } ]
|
||||
delete eolid
|
||||
forM_ (Map.toList newExamLabels) $ \(eoLabelIdent, (examOfficeLabelName, examOfficeLabelStatus, examOfficeLabelPriority)) -> case eoLabelIdent of
|
||||
Left _ -> void $ upsert ExamOfficeLabel{ examOfficeLabelUser=uid, .. }
|
||||
[ ExamOfficeLabelName =. examOfficeLabelName
|
||||
, ExamOfficeLabelStatus =. examOfficeLabelStatus
|
||||
, ExamOfficeLabelPriority =. examOfficeLabelPriority
|
||||
]
|
||||
Right lblId -> update lblId
|
||||
[ ExamOfficeLabelName =. examOfficeLabelName
|
||||
, ExamOfficeLabelStatus =. examOfficeLabelStatus
|
||||
, ExamOfficeLabelPriority =. examOfficeLabelPriority
|
||||
]
|
||||
addMessageI Success MsgSettingsUpdate
|
||||
redirect $ ProfileR :#: ProfileSettings
|
||||
|
||||
@ -454,7 +595,6 @@ getProfileDataR = do
|
||||
|
||||
makeProfileData :: Entity User -> DB Widget
|
||||
makeProfileData (Entity uid User{..}) = do
|
||||
-- MsgRenderer mr <- getMsgRenderer
|
||||
functions <- Map.fromListWith Set.union . map (\(Entity _ UserFunction{..}) -> (userFunctionFunction, Set.singleton userFunctionSchool)) <$> selectList [UserFunctionUser ==. uid] []
|
||||
lecture_corrector <- E.select $ E.distinct $ E.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do
|
||||
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
|
||||
@ -486,8 +626,8 @@ makeProfileData (Entity uid User{..}) = do
|
||||
|
||||
|
||||
|
||||
-- | Table listing all courses that the given user is a lecturer for
|
||||
mkOwnedCoursesTable :: UserId -> DB (Bool, Widget)
|
||||
-- Table listing all courses that the given user is a lecturer for
|
||||
mkOwnedCoursesTable =
|
||||
let dbtIdent = "courseOwnership" :: Text
|
||||
dbtStyle = def
|
||||
@ -537,9 +677,8 @@ mkOwnedCoursesTable =
|
||||
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..}
|
||||
|
||||
|
||||
|
||||
-- | Table listing all courses that the given user is enrolled in
|
||||
mkEnrolledCoursesTable :: UserId -> DB Widget
|
||||
-- Table listing all courses that the given user is enrolled in
|
||||
mkEnrolledCoursesTable =
|
||||
let withType :: ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant)) -> a)
|
||||
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant)) -> a)
|
||||
@ -590,9 +729,8 @@ mkEnrolledCoursesTable =
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- | Table listing all submissions for the given user
|
||||
mkSubmissionTable :: UserId -> DB Widget
|
||||
-- Table listing all submissions for the given user
|
||||
mkSubmissionTable =
|
||||
let dbtIdent = "submissions" :: Text
|
||||
dbtStyle = def
|
||||
@ -676,9 +814,8 @@ mkSubmissionTable =
|
||||
-- return $ dbTableWidget' validator $ DBTable {..}
|
||||
|
||||
|
||||
|
||||
-- | Table listing all submissions for the given user
|
||||
mkSubmissionGroupTable :: UserId -> DB Widget
|
||||
-- Table listing all submissions for the given user
|
||||
mkSubmissionGroupTable =
|
||||
let dbtIdent = "subGroups" :: Text
|
||||
dbtStyle = def
|
||||
@ -733,13 +870,10 @@ mkSubmissionGroupTable =
|
||||
in dbTableWidget' validator DBTable{..}
|
||||
|
||||
|
||||
|
||||
mkCorrectionsTable :: UserId -> DB Widget
|
||||
-- Table listing sum of corrections made by the given user per sheet
|
||||
mkCorrectionsTable =
|
||||
let dbtIdent = "corrections" :: Text
|
||||
dbtStyle = def
|
||||
-- TODO Continue here
|
||||
withType :: ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity SheetCorrector))->a)
|
||||
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity SheetCorrector))->a)
|
||||
withType = id
|
||||
@ -929,8 +1063,14 @@ getCsvOptionsR = postCsvOptionsR
|
||||
postCsvOptionsR = do
|
||||
Entity uid User{userCsvOptions} <- requireAuth
|
||||
|
||||
userIsExamOffice <- hasReadAccessTo $ ExamOfficeR EOExamsR
|
||||
examOfficeLabels <- if not userIsExamOffice then return mempty else runDB . E.select . E.from $ \examOfficeLabel -> do
|
||||
E.where_ $ examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
|
||||
E.orderBy [ E.asc (examOfficeLabel E.^. ExamOfficeLabelName) ]
|
||||
return $ examOfficeLabel E.^. ExamOfficeLabelName
|
||||
|
||||
((optionsRes, optionsWgt'), optionsEnctype) <- runFormPost . renderAForm FormStandard $
|
||||
csvOptionsForm (Just userCsvOptions)
|
||||
csvOptionsForm (Just userCsvOptions) (Set.fromList $ E.unValue <$> examOfficeLabels)
|
||||
|
||||
formResultModal optionsRes CsvOptionsR $ \opts -> do
|
||||
lift . runDB $ update uid [ UserCsvOptions =. opts ]
|
||||
|
||||
@ -75,6 +75,8 @@ postAdminUserAddR = do
|
||||
, userDownloadFiles = userDefaultDownloadFiles
|
||||
, userWarningDays = userDefaultWarningDays
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
, userNotificationSettings = def
|
||||
, userLanguages = Nothing
|
||||
, userCsvOptions = def
|
||||
|
||||
@ -135,8 +135,9 @@ data ExternalExamUserActionData
|
||||
| ExternalExamUserEditResultData ExamResultPassedGrade
|
||||
| ExternalExamUserDeleteData
|
||||
|
||||
newtype ExternalExamUserCsvExportDataGrades = ExternalExamUserCsvExportDataGrades
|
||||
data ExternalExamUserCsvExportDataGrades = ExternalExamUserCsvExportDataGrades
|
||||
{ csvEEUserMarkSynchronised :: Bool
|
||||
, csvEEUserSetLabel :: Bool
|
||||
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||
|
||||
|
||||
@ -192,12 +193,19 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
|
||||
coursen = externalExamCourseName
|
||||
examn = externalExamExamName
|
||||
|
||||
uid <- requireAuthId
|
||||
Entity uid currentUser <- requireAuth
|
||||
isLecturer <- hasReadAccessTo $ EExamR tid ssh coursen examn EEUsersR
|
||||
isExamOffice <- hasReadAccessTo $ ExamOfficeR EOExamsR
|
||||
currentRoute <- fromMaybe (error "makeExternalExamUsersTable called from 404-handler") <$> getCurrentRoute
|
||||
MsgRenderer mr <- getMsgRenderer
|
||||
exampleTime <- over _utctDayTime (fromInteger . round . toRational) <$> liftIO getCurrentTime
|
||||
|
||||
userCsvExportLabel' <- E.select . E.from $ \examOfficeLabel -> do
|
||||
E.where_ $ maybe E.false (\expLbl -> examOfficeLabel E.^. ExamOfficeLabelName E.==. E.val expLbl) (csvExportLabel $ userCsvOptions currentUser)
|
||||
E.&&. examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
|
||||
return examOfficeLabel
|
||||
let userCsvExportLabel = listToMaybe userCsvExportLabel'
|
||||
|
||||
let
|
||||
dbtSQLQuery = runReaderT $ do
|
||||
result <- view queryResult
|
||||
@ -245,7 +253,7 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
|
||||
colSynced = Colonnade.singleton (fromSortable . Sortable (Just "is-synced") $ i18nCell MsgExternalExamUserSynchronised) $ \x -> cell . flip runReaderT x $ do
|
||||
syncs <- asks $ sortOn (Down . view _3) . toListOf resultSynchronised
|
||||
lastChange <- view $ resultResult . _entityVal . _externalExamResultLastChanged
|
||||
user <- view $ resultUser . _entityVal
|
||||
User{..} <- view $ resultUser . _entityVal
|
||||
isSynced <- view resultIsSynced
|
||||
let
|
||||
hasSyncs = has folded syncs
|
||||
@ -363,8 +371,17 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
|
||||
EEUMGrades -> Just DBTCsvEncode
|
||||
{ dbtCsvExportForm = ExternalExamUserCsvExportDataGrades
|
||||
<$> apopt checkBoxField (fslI MsgExternalExamUserMarkSynchronisedCsv & setTooltip MsgExternalExamUserMarkSynchronisedCsvTip) (Just False)
|
||||
<*> bool
|
||||
( pure False )
|
||||
( maybe
|
||||
(aforced checkBoxField (fslI MsgExamOfficeLabelSetLabelOnExport & setTooltip MsgExamOfficeLabelSetLabelOnExportForcedTip) False)
|
||||
(\expLbl -> apopt checkBoxField (fslI MsgExamOfficeLabelSetLabelOnExport & setTooltip (MsgExamOfficeLabelSetLabelOnExportTip expLbl)) (Just True))
|
||||
(examOfficeLabelName . entityVal <$> userCsvExportLabel)
|
||||
)
|
||||
isExamOffice
|
||||
, dbtCsvDoEncode = \ExternalExamUserCsvExportDataGrades{..} -> C.mapM $ \(E.Value k, row) -> do
|
||||
when csvEEUserMarkSynchronised $ externalExamResultMarkSynchronised k
|
||||
when csvEEUserSetLabel $ maybe (return ()) (\lbl -> void $ upsert (ExamOfficeExternalExamLabel eeId lbl) [ExamOfficeExternalExamLabelLabel =. lbl]) (entityKey <$> userCsvExportLabel)
|
||||
return $ encodeCsv' row
|
||||
, dbtCsvName, dbtCsvSheetName
|
||||
, dbtCsvNoExportData = Nothing
|
||||
|
||||
@ -2125,10 +2125,18 @@ csvOptionsForm :: forall m.
|
||||
, HandlerSite m ~ UniWorX
|
||||
)
|
||||
=> Maybe CsvOptions
|
||||
-> Set ExamOfficeLabelName
|
||||
-> AForm m CsvOptions
|
||||
csvOptionsForm mPrev = hoistAForm liftHandler $ CsvOptions
|
||||
csvOptionsForm mPrev (Set.toList -> exportLabels) = hoistAForm liftHandler $ CsvOptions
|
||||
<$> csvFormatOptionsForm (fslI MsgCsvFormatOptions & setTooltip MsgCsvOptionsTip) (csvFormat <$> mPrev)
|
||||
<*> apopt checkBoxField (fslI MsgCsvTimestamp & setTooltip MsgCsvTimestampTip) (csvTimestamp <$> mPrev)
|
||||
<*> bool (aopt (selectField $ return exportLabelOptions) (fslI MsgCsvExportLabel & setTooltip MsgCsvExportLabelTip) (csvExportLabel <$> mPrev)) (pure Nothing) (null exportLabels)
|
||||
where
|
||||
exportLabelOptions = mkOptionList $ exportLabels <&> \exportLabel -> Option
|
||||
{ optionDisplay = exportLabel
|
||||
, optionInternalValue = exportLabel
|
||||
, optionExternalValue = exportLabel
|
||||
}
|
||||
|
||||
|
||||
courseSelectForm :: forall ident handler.
|
||||
|
||||
@ -248,6 +248,15 @@ colExamFinishedOffice resultFinished = Colonnade.singleton (fromSortable header)
|
||||
sortExamFinished :: OpticSortColumn (Maybe UTCTime)
|
||||
sortExamFinished queryFinished = singletonMap "exam-finished" . SortColumn $ view queryFinished
|
||||
|
||||
colExamLabel :: OpticColonnade (Maybe ExamOfficeLabelName)
|
||||
colExamLabel resultLabel = Colonnade.singleton (fromSortable header) body
|
||||
where
|
||||
header = Sortable (Just "exam-label") (i18nCell MsgTableExamLabel)
|
||||
body = views resultLabel $ maybe mempty i18nCell
|
||||
|
||||
sortExamLabel :: OpticSortColumn (Maybe ExamOfficeLabelName)
|
||||
sortExamLabel queryLabel = singletonMap "exam-label" . SortColumn $ view queryLabel
|
||||
|
||||
---------------------
|
||||
-- Exam occurences --
|
||||
---------------------
|
||||
|
||||
@ -6,6 +6,7 @@ import Model.Types.Common as Types
|
||||
import Model.Types.Course as Types
|
||||
import Model.Types.DateTime as Types
|
||||
import Model.Types.Exam as Types
|
||||
import Model.Types.ExamOffice as Types
|
||||
import Model.Types.Health as Types
|
||||
import Model.Types.Mail as Types
|
||||
import Model.Types.Security as Types
|
||||
|
||||
@ -51,8 +51,9 @@ nullaryPathPiece ''Quoting $ \q -> if
|
||||
|
||||
data CsvOptions
|
||||
= CsvOptions
|
||||
{ csvFormat :: CsvFormatOptions
|
||||
, csvTimestamp :: Bool
|
||||
{ csvFormat :: CsvFormatOptions
|
||||
, csvTimestamp :: Bool
|
||||
, csvExportLabel :: Maybe Text
|
||||
}
|
||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||
deriving anyclass (Hashable, NFData)
|
||||
@ -73,8 +74,9 @@ makeLenses_ ''CsvFormatOptions
|
||||
|
||||
instance Default CsvOptions where
|
||||
def = CsvOptions
|
||||
{ csvFormat = def
|
||||
, csvTimestamp = False
|
||||
{ csvFormat = def
|
||||
, csvTimestamp = False
|
||||
, csvExportLabel = Nothing
|
||||
}
|
||||
|
||||
instance Default CsvFormatOptions where
|
||||
@ -128,14 +130,16 @@ _CsvEncodeOptions = prism' fromEncode toEncode
|
||||
|
||||
instance ToJSON CsvOptions where
|
||||
toJSON CsvOptions{..} = JSON.object
|
||||
[ "format" JSON..= csvFormat
|
||||
, "timestamp" JSON..= csvTimestamp
|
||||
[ "format" JSON..= csvFormat
|
||||
, "timestamp" JSON..= csvTimestamp
|
||||
, "export-label" JSON..= csvExportLabel
|
||||
]
|
||||
|
||||
instance FromJSON CsvOptions where
|
||||
parseJSON = JSON.withObject "CsvOptions" $ \o -> do
|
||||
csvFormat <- o JSON..:? "format" JSON..!= csvFormat def
|
||||
csvTimestamp <- o JSON..:? "timestamp" JSON..!= csvTimestamp def
|
||||
csvFormat <- o JSON..:? "format" JSON..!= csvFormat def
|
||||
csvTimestamp <- o JSON..:? "timestamp" JSON..!= csvTimestamp def
|
||||
csvExportLabel <- o JSON..:? "export-label" JSON..!= csvExportLabel def
|
||||
return CsvOptions{..}
|
||||
|
||||
data CsvFormat = FormatCsv | FormatXlsx
|
||||
|
||||
8
src/Model/Types/ExamOffice.hs
Normal file
8
src/Model/Types/ExamOffice.hs
Normal file
@ -0,0 +1,8 @@
|
||||
module Model.Types.ExamOffice
|
||||
( ExamOfficeLabelName
|
||||
) where
|
||||
|
||||
import Import.NoModel
|
||||
|
||||
|
||||
type ExamOfficeLabelName = Text
|
||||
@ -257,6 +257,8 @@ data UserDefaultConf = UserDefaultConf
|
||||
, userDefaultDownloadFiles :: Bool
|
||||
, userDefaultWarningDays :: NominalDiffTime
|
||||
, userDefaultShowSex :: Bool
|
||||
, userDefaultExamOfficeGetSynced :: Bool
|
||||
, userDefaultExamOfficeGetLabels :: Bool
|
||||
} deriving (Show)
|
||||
|
||||
data PWHashConf = PWHashConf
|
||||
|
||||
@ -30,10 +30,11 @@ notification nType Message{ messageIcon = messageIcon', .. }
|
||||
where
|
||||
messageIcon = fromMaybe defaultIcon messageIcon'
|
||||
defaultIcon = case messageStatus of
|
||||
Success -> IconNotificationSuccess
|
||||
Info -> IconNotificationInfo
|
||||
Warning -> IconNotificationWarning
|
||||
Error -> IconNotificationError
|
||||
Success -> IconNotificationSuccess
|
||||
Info -> IconNotificationInfo
|
||||
Warning -> IconNotificationWarning
|
||||
Error -> IconNotificationError
|
||||
Nonactive -> IconNotificationNonactive
|
||||
|
||||
notificationWidget :: Yesod site
|
||||
=> NotificationType
|
||||
|
||||
@ -72,6 +72,7 @@ data Icon
|
||||
| IconNotificationInfo
|
||||
| IconNotificationWarning
|
||||
| IconNotificationError
|
||||
| IconNotificationNonactive
|
||||
| IconFavourite
|
||||
| IconLanguage
|
||||
| IconNavContainerClose | IconPageActionChildrenClose
|
||||
@ -150,6 +151,7 @@ iconText = \case
|
||||
IconNotificationInfo -> "info-circle"
|
||||
IconNotificationWarning -> "exclamation-circle"
|
||||
IconNotificationError -> "exclamation-triangle"
|
||||
IconNotificationNonactive -> "info"
|
||||
IconFavourite -> "star"
|
||||
IconLanguage -> "flag-alt"
|
||||
IconNavContainerClose -> "chevron-up"
|
||||
|
||||
@ -215,6 +215,8 @@ makeLenses_ ''UTCTime
|
||||
makeLenses_ ''Exam
|
||||
makeLenses_ ''ExamOccurrence
|
||||
|
||||
makeLenses_ ''ExamOfficeLabel
|
||||
|
||||
makePrisms ''AuthenticationMode
|
||||
|
||||
makeLenses_ ''CourseUserNote
|
||||
|
||||
@ -29,7 +29,7 @@ import Text.Blaze.Html.Renderer.Text (renderHtml)
|
||||
import Text.HTML.SanitizeXSS (sanitizeBalance)
|
||||
|
||||
|
||||
data MessageStatus = Error | Warning | Info | Success
|
||||
data MessageStatus = Error | Warning | Info | Success | Nonactive
|
||||
deriving (Eq, Ord, Enum, Bounded, Show, Read, Lift, Generic, Typeable)
|
||||
deriving anyclass (Universe, Finite, NFData)
|
||||
|
||||
@ -219,7 +219,7 @@ messageTooltip Message{..} = let urgency = statusToUrgencyClass messageStatus
|
||||
Utils.Message.Error -> IconProblem
|
||||
Utils.Message.Warning -> IconWarning
|
||||
Utils.Message.Success -> IconOK
|
||||
Utils.Message.Info -> IconTooltipDefault)
|
||||
_ -> IconTooltipDefault)
|
||||
messageIcon
|
||||
tooltip = toWidget messageContent :: WidgetFor site ()
|
||||
isInlineTooltip = False
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
$newline never
|
||||
<h1>
|
||||
^{nameWidget (userDisplayName user) (userSurname user)}
|
||||
^{nameWidget userDisplayName userSurname}
|
||||
|
||||
<table .table .table--striped .table--hover>
|
||||
<thead>
|
||||
|
||||
6
templates/profile/exam-office-labels/add.hamlet
Normal file
6
templates/profile/exam-office-labels/add.hamlet
Normal file
@ -0,0 +1,6 @@
|
||||
$newline never
|
||||
<td colspan=3>
|
||||
#{csrf}
|
||||
^{fvWidget addView}
|
||||
<td>
|
||||
^{fvWidget submitView}
|
||||
8
templates/profile/exam-office-labels/cell.hamlet
Normal file
8
templates/profile/exam-office-labels/cell.hamlet
Normal file
@ -0,0 +1,8 @@
|
||||
$newline never
|
||||
<td .table__td>
|
||||
^{labelIdent}
|
||||
<td .table__td>
|
||||
#{csrf}
|
||||
^{fvWidget statusView}
|
||||
<td .table__td>
|
||||
^{fvWidget priorityView}
|
||||
17
templates/profile/exam-office-labels/layout.hamlet
Normal file
17
templates/profile/exam-office-labels/layout.hamlet
Normal file
@ -0,0 +1,17 @@
|
||||
$newline never
|
||||
<table .table .table--striped .table--hover>
|
||||
<thead>
|
||||
<tr .table__row .table__row--head>
|
||||
<th .table__th>_{MsgTableExamOfficeLabel}
|
||||
<th .table__th>_{MsgTableExamOfficeLabelStatus}
|
||||
<th .table__th>_{MsgTableExamOfficeLabelPriority}
|
||||
<td>
|
||||
<tbody>
|
||||
$forall coord <- review liveCoords lLength
|
||||
<tr .massinput__cell .table__row>
|
||||
^{cellWdgts ! coord}
|
||||
<td>
|
||||
^{fvWidget (delButtons ! coord)}
|
||||
<tfoot>
|
||||
<tr .massinput__cell.massinput__cell--add>
|
||||
^{addWdgets ! (0, 0)}
|
||||
4
templates/widgets/exam-office-label.hamlet
Normal file
4
templates/widgets/exam-office-label.hamlet
Normal file
@ -0,0 +1,4 @@
|
||||
$newline never
|
||||
|
||||
<div .exam-office-label .#{toPathPiece examOfficeLabelStatus}>
|
||||
#{examOfficeLabelName}
|
||||
@ -150,6 +150,8 @@ fillDb = do
|
||||
, userCsvOptions = def { csvFormat = csvPreset # CsvPresetRFC }
|
||||
, userSex = Just SexMale
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
}
|
||||
fhamann <- insert User
|
||||
{ userIdent = "felix.hamann@campus.lmu.de"
|
||||
@ -179,6 +181,8 @@ fillDb = do
|
||||
, userCsvOptions = def { csvFormat = csvPreset # CsvPresetExcel }
|
||||
, userSex = Just SexMale
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
}
|
||||
jost <- insert User
|
||||
{ userIdent = "jost@tcs.ifi.lmu.de"
|
||||
@ -208,6 +212,8 @@ fillDb = do
|
||||
, userSex = Just SexMale
|
||||
, userCsvOptions = def
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
}
|
||||
maxMuster <- insert User
|
||||
{ userIdent = "max@campus.lmu.de"
|
||||
@ -237,6 +243,8 @@ fillDb = do
|
||||
, userCsvOptions = def
|
||||
, userSex = Just SexMale
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
}
|
||||
tinaTester <- insert $ User
|
||||
{ userIdent = "tester@campus.lmu.de"
|
||||
@ -266,6 +274,8 @@ fillDb = do
|
||||
, userCsvOptions = def
|
||||
, userSex = Just SexNotApplicable
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
}
|
||||
svaupel <- insert User
|
||||
{ userIdent = "vaupel.sarah@campus.lmu.de"
|
||||
@ -295,6 +305,8 @@ fillDb = do
|
||||
, userCsvOptions = def
|
||||
, userSex = Just SexFemale
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
}
|
||||
sbarth <- insert User
|
||||
{ userIdent = "Stephan.Barth@campus.lmu.de"
|
||||
@ -324,6 +336,8 @@ fillDb = do
|
||||
, userCsvOptions = def
|
||||
, userSex = Just SexMale
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = False
|
||||
, userExamOfficeGetLabels = True
|
||||
}
|
||||
|
||||
let
|
||||
@ -383,6 +397,8 @@ fillDb = do
|
||||
, userCsvOptions = def
|
||||
, userSex = Nothing
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
}
|
||||
where
|
||||
userIdent :: IsString t => t
|
||||
@ -472,6 +488,22 @@ fillDb = do
|
||||
void . insert' $ UserSchool uid ifi False
|
||||
for_ [gkleen, tinaTester] $ \uid ->
|
||||
void . insert' $ UserSchool uid mi False
|
||||
|
||||
let
|
||||
examLabels = Map.fromList
|
||||
[ ( sbarth
|
||||
, [ ("In Bearbeitung" , Success , 4)
|
||||
, ("Sonderfall" , Warning , 1)
|
||||
, ("Zu überprüfen" , Error , 1)
|
||||
, ("Weiterzuleiten" , Info , 3)
|
||||
, ("Nicht zu bearbeiten" , Nonactive , -1)
|
||||
]
|
||||
)
|
||||
]
|
||||
for_ (Map.toList examLabels) $ \(examOfficeLabelUser, labels) ->
|
||||
for_ labels $ \(examOfficeLabelName, examOfficeLabelStatus, examOfficeLabelPriority) ->
|
||||
void $ insert' ExamOfficeLabel{..}
|
||||
|
||||
let
|
||||
sdBsc = StudyDegreeKey' 82
|
||||
sdMst = StudyDegreeKey' 88
|
||||
|
||||
@ -298,6 +298,7 @@ instance Arbitrary CsvOptions where
|
||||
arbitrary = CsvOptions
|
||||
<$> arbitrary
|
||||
<*> arbitrary
|
||||
<*> suchThat arbitrary (maybe True $ not . elem (Char.chr 0))
|
||||
shrink = genericShrink
|
||||
|
||||
instance Arbitrary CsvPreset where
|
||||
|
||||
@ -128,6 +128,8 @@ instance Arbitrary User where
|
||||
userNotificationSettings <- arbitrary
|
||||
userCsvOptions <- arbitrary
|
||||
userShowSex <- arbitrary
|
||||
userExamOfficeGetSynced <- arbitrary
|
||||
userExamOfficeGetLabels <- arbitrary
|
||||
|
||||
userCreated <- arbitrary
|
||||
userLastLdapSynchronisation <- arbitrary
|
||||
|
||||
@ -37,6 +37,8 @@ fakeUser adjUser = adjUser User{..}
|
||||
userLanguages = Nothing
|
||||
userWarningDays = userDefaultWarningDays
|
||||
userCsvOptions = def
|
||||
userExamOfficeGetSynced = True
|
||||
userExamOfficeGetLabels = True
|
||||
userSex = Nothing
|
||||
userShowSex = userDefaultShowSex
|
||||
userNotificationSettings = def
|
||||
|
||||
Reference in New Issue
Block a user