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
+
+
+
+ | _{MsgTableExamOfficeLabel}
+ | _{MsgTableExamOfficeLabelStatus}
+ | _{MsgTableExamOfficeLabelPriority}
+ |
+ |
+ $forall coord <- review liveCoords lLength
+
+ ^{cellWdgts ! coord}
+ |
+ ^{fvWidget (delButtons ! 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
|