diff --git a/config/settings.yml b/config/settings.yml index 535504e62..be47423b4 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -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. diff --git a/frontend/src/app.sass b/frontend/src/app.sass index 09c31d052..de06febd1 100644 --- a/frontend/src/app.sass +++ b/frontend/src/app.sass @@ -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) diff --git a/messages/uniworx/categories/courses/exam/exam_office/de-de-formal.msg b/messages/uniworx/categories/courses/exam/exam_office/de-de-formal.msg index ee5a84a00..db7a4baa0 100644 --- a/messages/uniworx/categories/courses/exam/exam_office/de-de-formal.msg +++ b/messages/uniworx/categories/courses/exam/exam_office/de-de-formal.msg @@ -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. diff --git a/messages/uniworx/categories/courses/exam/exam_office/en-eu.msg b/messages/uniworx/categories/courses/exam/exam_office/en-eu.msg index 99ccc888c..397e21d7f 100644 --- a/messages/uniworx/categories/courses/exam/exam_office/en-eu.msg +++ b/messages/uniworx/categories/courses/exam/exam_office/en-eu.msg @@ -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. diff --git a/messages/uniworx/categories/settings/csv_options/de-de-formal.msg b/messages/uniworx/categories/settings/csv_options/de-de-formal.msg index 5041a5918..31d411eb0 100644 --- a/messages/uniworx/categories/settings/csv_options/de-de-formal.msg +++ b/messages/uniworx/categories/settings/csv_options/de-de-formal.msg @@ -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 diff --git a/messages/uniworx/categories/settings/csv_options/en-eu.msg b/messages/uniworx/categories/settings/csv_options/en-eu.msg index 2900dc672..2f9d76ab8 100644 --- a/messages/uniworx/categories/settings/csv_options/en-eu.msg +++ b/messages/uniworx/categories/settings/csv_options/en-eu.msg @@ -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 diff --git a/messages/uniworx/categories/settings/de-de-formal.msg b/messages/uniworx/categories/settings/de-de-formal.msg index 1c92b705e..9f45045b0 100644 --- a/messages/uniworx/categories/settings/de-de-formal.msg +++ b/messages/uniworx/categories/settings/de-de-formal.msg @@ -112,3 +112,5 @@ AllocNotifyNewCourseDefault: Systemweite Einstellung AllocNotifyNewCourseForceOff: Nein AllocNotifyNewCourseForceOn: Ja Settings: Individuelle Benutzereinstellungen + +FormExamOffice: Prüfungsverwaltung diff --git a/messages/uniworx/categories/settings/en-eu.msg b/messages/uniworx/categories/settings/en-eu.msg index fe8374754..d94b9b7cc 100644 --- a/messages/uniworx/categories/settings/en-eu.msg +++ b/messages/uniworx/categories/settings/en-eu.msg @@ -112,4 +112,6 @@ LanguageChanged: Language changed successfully AllocNotifyNewCourseDefault: System-wide setting AllocNotifyNewCourseForceOff: No AllocNotifyNewCourseForceOn: Yes -Settings: Settings \ No newline at end of file +Settings: Settings + +FormExamOffice: Exam Office \ No newline at end of file diff --git a/messages/uniworx/utils/table_column/de-de-formal.msg b/messages/uniworx/utils/table_column/de-de-formal.msg index e91267835..6077dcfc3 100644 --- a/messages/uniworx/utils/table_column/de-de-formal.msg +++ b/messages/uniworx/utils/table_column/de-de-formal.msg @@ -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 \ No newline at end of file +TableExamFinished: Ergebnisse sichtbar ab +TableExamOfficeLabel: Label-Name +TableExamOfficeLabelStatus: Label-Farbe +TableExamOfficeLabelPriority: Label-Priorität \ No newline at end of file diff --git a/messages/uniworx/utils/table_column/en-eu.msg b/messages/uniworx/utils/table_column/en-eu.msg index 5913fddca..627867eb7 100644 --- a/messages/uniworx/utils/table_column/en-eu.msg +++ b/messages/uniworx/utils/table_column/en-eu.msg @@ -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 \ No newline at end of file +TableExamFinished: Results visible from +TableExamOfficeLabel: Label name +TableExamOfficeLabelStatus: Label colour +TableExamOfficeLabelPriority: Label priority \ No newline at end of file diff --git a/messages/uniworx/utils/utils/de-de-formal.msg b/messages/uniworx/utils/utils/de-de-formal.msg index 6e80466d9..615126c14 100644 --- a/messages/uniworx/utils/utils/de-de-formal.msg +++ b/messages/uniworx/utils/utils/de-de-formal.msg @@ -136,6 +136,7 @@ MessageError: Fehler MessageWarning: Warnung MessageInfo !ident-ok: Information MessageSuccess: Erfolg +MessageNonactive: Inaktiv ShortFieldPrimary: HF ShortFieldSecondary: NF SheetGradingPassPoints': Bestehen nach Punkten diff --git a/messages/uniworx/utils/utils/en-eu.msg b/messages/uniworx/utils/utils/en-eu.msg index 652674005..6f1ecad76 100644 --- a/messages/uniworx/utils/utils/en-eu.msg +++ b/messages/uniworx/utils/utils/en-eu.msg @@ -136,6 +136,7 @@ MessageError: Error MessageWarning: Warning MessageInfo: Information MessageSuccess: Success +MessageNonactive: Inactive ShortFieldPrimary: Mj ShortFieldSecondary: Mn SheetGradingPassPoints': Passing by points diff --git a/models/exam-office/exam-labels.model b/models/exam-office/exam-labels.model new file mode 100644 index 000000000..a22a8ebc7 --- /dev/null +++ b/models/exam-office/exam-labels.model @@ -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 diff --git a/models/users.model b/models/users.model index 707da5e2f..80846e952 100644 --- a/models/users.model +++ b/models/users.model @@ -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" diff --git a/routes b/routes index 8051d646f..f5083251c 100644 --- a/routes +++ b/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 diff --git a/src/Foundation/Yesod/Auth.hs b/src/Foundation/Yesod/Auth.hs index 29c77c654..17042dbb6 100644 --- a/src/Foundation/Yesod/Auth.hs +++ b/src/Foundation/Yesod/Auth.hs @@ -257,6 +257,8 @@ upsertCampusUser upsertMode ldapData = do , userDownloadFiles = userDefaultDownloadFiles , userWarningDays = userDefaultWarningDays , userShowSex = userDefaultShowSex + , userExamOfficeGetSynced = userDefaultExamOfficeGetSynced + , userExamOfficeGetLabels = userDefaultExamOfficeGetLabels , userNotificationSettings = def , userLanguages = Nothing , userCsvOptions = def diff --git a/src/Handler/ExamOffice/Exam.hs b/src/Handler/ExamOffice/Exam.hs index cc631665f..cde8a0015 100644 --- a/src/Handler/ExamOffice/Exam.hs +++ b/src/Handler/ExamOffice/Exam.hs @@ -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) diff --git a/src/Handler/ExamOffice/Exams.hs b/src/Handler/ExamOffice/Exams.hs index f98eac37f..c6ec5ee37 100644 --- a/src/Handler/ExamOffice/Exams.hs +++ b/src/Handler/ExamOffice/Exams.hs @@ -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 diff --git a/src/Handler/Profile.hs b/src/Handler/Profile.hs index 9b7dc1ee0..4a7cdcbab 100644 --- a/src/Handler/Profile.hs +++ b/src/Handler/Profile.hs @@ -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 ] diff --git a/src/Handler/Users/Add.hs b/src/Handler/Users/Add.hs index 01196e7ec..aa64839cf 100644 --- a/src/Handler/Users/Add.hs +++ b/src/Handler/Users/Add.hs @@ -75,6 +75,8 @@ postAdminUserAddR = do , userDownloadFiles = userDefaultDownloadFiles , userWarningDays = userDefaultWarningDays , userShowSex = userDefaultShowSex + , userExamOfficeGetSynced = userDefaultExamOfficeGetSynced + , userExamOfficeGetLabels = userDefaultExamOfficeGetLabels , userNotificationSettings = def , userLanguages = Nothing , userCsvOptions = def diff --git a/src/Handler/Utils/ExternalExam/Users.hs b/src/Handler/Utils/ExternalExam/Users.hs index 70a20fec6..9823e1905 100644 --- a/src/Handler/Utils/ExternalExam/Users.hs +++ b/src/Handler/Utils/ExternalExam/Users.hs @@ -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 diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs index 92940d471..b39d89428 100644 --- a/src/Handler/Utils/Form.hs +++ b/src/Handler/Utils/Form.hs @@ -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. diff --git a/src/Handler/Utils/Table/Columns.hs b/src/Handler/Utils/Table/Columns.hs index e13284064..fd0e2c4a8 100644 --- a/src/Handler/Utils/Table/Columns.hs +++ b/src/Handler/Utils/Table/Columns.hs @@ -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 -- --------------------- diff --git a/src/Model/Types.hs b/src/Model/Types.hs index ac591631c..0de01da40 100644 --- a/src/Model/Types.hs +++ b/src/Model/Types.hs @@ -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 diff --git a/src/Model/Types/Csv.hs b/src/Model/Types/Csv.hs index 88f183de9..ca7ec802b 100644 --- a/src/Model/Types/Csv.hs +++ b/src/Model/Types/Csv.hs @@ -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 diff --git a/src/Model/Types/ExamOffice.hs b/src/Model/Types/ExamOffice.hs new file mode 100644 index 000000000..60dc51bbf --- /dev/null +++ b/src/Model/Types/ExamOffice.hs @@ -0,0 +1,8 @@ +module Model.Types.ExamOffice + ( ExamOfficeLabelName + ) where + +import Import.NoModel + + +type ExamOfficeLabelName = Text diff --git a/src/Settings.hs b/src/Settings.hs index af10c98f4..b056dfc1f 100644 --- a/src/Settings.hs +++ b/src/Settings.hs @@ -257,6 +257,8 @@ data UserDefaultConf = UserDefaultConf , userDefaultDownloadFiles :: Bool , userDefaultWarningDays :: NominalDiffTime , userDefaultShowSex :: Bool + , userDefaultExamOfficeGetSynced :: Bool + , userDefaultExamOfficeGetLabels :: Bool } deriving (Show) data PWHashConf = PWHashConf diff --git a/src/Utils/Frontend/Notification.hs b/src/Utils/Frontend/Notification.hs index d4ec0758a..395bbf144 100644 --- a/src/Utils/Frontend/Notification.hs +++ b/src/Utils/Frontend/Notification.hs @@ -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 diff --git a/src/Utils/Icon.hs b/src/Utils/Icon.hs index d220f9f7f..f9fdc4234 100644 --- a/src/Utils/Icon.hs +++ b/src/Utils/Icon.hs @@ -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" diff --git a/src/Utils/Lens.hs b/src/Utils/Lens.hs index 59f8266fa..8464e5b36 100644 --- a/src/Utils/Lens.hs +++ b/src/Utils/Lens.hs @@ -215,6 +215,8 @@ makeLenses_ ''UTCTime makeLenses_ ''Exam makeLenses_ ''ExamOccurrence +makeLenses_ ''ExamOfficeLabel + makePrisms ''AuthenticationMode makeLenses_ ''CourseUserNote diff --git a/src/Utils/Message.hs b/src/Utils/Message.hs index 27ccafe41..61bce17c7 100644 --- a/src/Utils/Message.hs +++ b/src/Utils/Message.hs @@ -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 diff --git a/templates/exam-office/exam-result-synced.hamlet b/templates/exam-office/exam-result-synced.hamlet index a121c879e..e60f7d809 100644 --- a/templates/exam-office/exam-result-synced.hamlet +++ b/templates/exam-office/exam-result-synced.hamlet @@ -1,6 +1,6 @@ $newline never

- ^{nameWidget (userDisplayName user) (userSurname user)} + ^{nameWidget userDisplayName userSurname} diff --git a/templates/profile/exam-office-labels/add.hamlet b/templates/profile/exam-office-labels/add.hamlet new file mode 100644 index 000000000..2935c0b46 --- /dev/null +++ b/templates/profile/exam-office-labels/add.hamlet @@ -0,0 +1,6 @@ +$newline never +
+ #{csrf} + ^{fvWidget addView} + + ^{fvWidget submitView} diff --git a/templates/profile/exam-office-labels/cell.hamlet b/templates/profile/exam-office-labels/cell.hamlet new file mode 100644 index 000000000..32d4fab7b --- /dev/null +++ b/templates/profile/exam-office-labels/cell.hamlet @@ -0,0 +1,8 @@ +$newline never + + ^{labelIdent} + + #{csrf} + ^{fvWidget statusView} + + ^{fvWidget priorityView} diff --git a/templates/profile/exam-office-labels/layout.hamlet b/templates/profile/exam-office-labels/layout.hamlet new file mode 100644 index 000000000..95f9a9c00 --- /dev/null +++ b/templates/profile/exam-office-labels/layout.hamlet @@ -0,0 +1,17 @@ +$newline never + + + + + $forall coord <- review liveCoords lLength + + ^{cellWdgts ! coord} + + + ^{addWdgets ! (0, 0)} diff --git a/templates/widgets/exam-office-label.hamlet b/templates/widgets/exam-office-label.hamlet new file mode 100644 index 000000000..db8d43354 --- /dev/null +++ b/templates/widgets/exam-office-label.hamlet @@ -0,0 +1,4 @@ +$newline never + +
+ #{examOfficeLabelName} diff --git a/test/Database/Fill.hs b/test/Database/Fill.hs index b57095456..4ed996c40 100644 --- a/test/Database/Fill.hs +++ b/test/Database/Fill.hs @@ -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 diff --git a/test/Model/TypesSpec.hs b/test/Model/TypesSpec.hs index fad24d3f7..10da379ed 100644 --- a/test/Model/TypesSpec.hs +++ b/test/Model/TypesSpec.hs @@ -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 diff --git a/test/ModelSpec.hs b/test/ModelSpec.hs index 66b90b480..27a5bed3f 100644 --- a/test/ModelSpec.hs +++ b/test/ModelSpec.hs @@ -128,6 +128,8 @@ instance Arbitrary User where userNotificationSettings <- arbitrary userCsvOptions <- arbitrary userShowSex <- arbitrary + userExamOfficeGetSynced <- arbitrary + userExamOfficeGetLabels <- arbitrary userCreated <- arbitrary userLastLdapSynchronisation <- arbitrary diff --git a/test/User.hs b/test/User.hs index 35ba6a848..ff7e14c62 100644 --- a/test/User.hs +++ b/test/User.hs @@ -37,6 +37,8 @@ fakeUser adjUser = adjUser User{..} userLanguages = Nothing userWarningDays = userDefaultWarningDays userCsvOptions = def + userExamOfficeGetSynced = True + userExamOfficeGetLabels = True userSex = Nothing userShowSex = userDefaultShowSex userNotificationSettings = def
_{MsgTableExamOfficeLabel} + _{MsgTableExamOfficeLabelStatus} + _{MsgTableExamOfficeLabelPriority} + +
+ ^{fvWidget (delButtons ! coord)} +