Merge branch '740-labels' into 'master'

Resolve "Labels"

See merge request uni2work/uni2work!74
This commit is contained in:
Sarah Vaupel 2022-02-04 02:06:54 +01:00
commit fa0744172e
40 changed files with 691 additions and 124 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -112,3 +112,5 @@ AllocNotifyNewCourseDefault: Systemweite Einstellung
AllocNotifyNewCourseForceOff: Nein
AllocNotifyNewCourseForceOn: Ja
Settings: Individuelle Benutzereinstellungen
FormExamOffice: Prüfungsverwaltung

View File

@ -112,4 +112,6 @@ LanguageChanged: Language changed successfully
AllocNotifyNewCourseDefault: System-wide setting
AllocNotifyNewCourseForceOff: No
AllocNotifyNewCourseForceOn: Yes
Settings: Settings
Settings: Settings
FormExamOffice: Exam Office

View File

@ -22,6 +22,7 @@ TableExamName !ident-ok: Name
TableExamTime: Termin
TableExamRegistration: Prüfungsanmeldung
TableExamResult: Prüfungsergebnis
TableExamLabel !ident-ok: Label
TableSheet: Blatt
TableLastEdit: Letzte Änderung
TableSubmission: Abgabenummer
@ -61,4 +62,7 @@ SelectColumn: Auswahl
CsvExport: CSV-Export
TableProportion c@Text of'@Text prop@Rational !ident-ok: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
TableProportionNoRatio c@Text of'@Text !ident-ok: #{c}/#{of'}
TableExamFinished: Ergebnisse sichtbar ab
TableExamFinished: Ergebnisse sichtbar ab
TableExamOfficeLabel: Label-Name
TableExamOfficeLabelStatus: Label-Farbe
TableExamOfficeLabelPriority: Label-Priorität

View File

@ -22,6 +22,7 @@ TableExamName: Name
TableExamTime: Time
TableExamRegistration: Exam registration
TableExamResult: Exam result
TableExamLabel: Label
TableSheet: Sheet
TableLastEdit: Latest edit
TableSubmission: Submission-number
@ -61,4 +62,7 @@ SelectColumn: Selection
CsvExport: CSV export
TableProportion c of' prop: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
TableProportionNoRatio c of': #{c}/#{of'}
TableExamFinished: Results visible from
TableExamFinished: Results visible from
TableExamOfficeLabel: Label name
TableExamOfficeLabelStatus: Label colour
TableExamOfficeLabelPriority: Label priority

View File

@ -136,6 +136,7 @@ MessageError: Fehler
MessageWarning: Warnung
MessageInfo !ident-ok: Information
MessageSuccess: Erfolg
MessageNonactive: Inaktiv
ShortFieldPrimary: HF
ShortFieldSecondary: NF
SheetGradingPassPoints': Bestehen nach Punkten

View File

@ -136,6 +136,7 @@ MessageError: Error
MessageWarning: Warning
MessageInfo: Information
MessageSuccess: Success
MessageNonactive: Inactive
ShortFieldPrimary: Mj
ShortFieldSecondary: Mn
SheetGradingPassPoints': Passing by points

View File

@ -0,0 +1,18 @@
ExamOfficeLabel
user UserId
name ExamOfficeLabelName
status MessageStatus
priority Int -- determines label ordering
UniqueExamOfficeLabel user name
deriving Generic
ExamOfficeExamLabel
exam ExamId
label ExamOfficeLabelId
UniqueExamOfficeExamLabel exam
deriving Generic
ExamOfficeExternalExamLabel
externalExam ExternalExamId
label ExamOfficeLabelId
UniqueExamOfficeExternalExamLabel externalExam
deriving Generic

View File

@ -35,6 +35,8 @@ User json -- Each Uni2work user has a corresponding row in this table; create
csvOptions CsvOptions "default='{}'::jsonb"
sex Sex Maybe
showSex Bool default=false
examOfficeGetSynced Bool default=true -- whether synced status should be displayed for exam results by default
examOfficeGetLabels Bool default=true -- whether labels should be displayed for exam results by default
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
UniqueEmail email -- Column 'email' can be used as a row-key in this table
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
@ -53,8 +55,8 @@ UserSystemFunction
UniqueUserSystemFunction user function
deriving Generic
UserExamOffice
user UserId
field StudyTermsId
user UserId
field StudyTermsId
UniqueUserExamOffice user field
deriving Generic
UserSchool -- Managed by users themselves, encodes "schools of interest"

2
routes
View File

@ -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

View File

@ -257,6 +257,8 @@ upsertCampusUser upsertMode ldapData = do
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
, userNotificationSettings = def
, userLanguages = Nothing
, userCsvOptions = def

View File

@ -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)

View File

@ -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

View File

@ -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 ]

View File

@ -75,6 +75,8 @@ postAdminUserAddR = do
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
, userNotificationSettings = def
, userLanguages = Nothing
, userCsvOptions = def

View File

@ -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

View File

@ -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.

View File

@ -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 --
---------------------

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,8 @@
module Model.Types.ExamOffice
( ExamOfficeLabelName
) where
import Import.NoModel
type ExamOfficeLabelName = Text

View File

@ -257,6 +257,8 @@ data UserDefaultConf = UserDefaultConf
, userDefaultDownloadFiles :: Bool
, userDefaultWarningDays :: NominalDiffTime
, userDefaultShowSex :: Bool
, userDefaultExamOfficeGetSynced :: Bool
, userDefaultExamOfficeGetLabels :: Bool
} deriving (Show)
data PWHashConf = PWHashConf

View File

@ -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

View File

@ -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"

View File

@ -215,6 +215,8 @@ makeLenses_ ''UTCTime
makeLenses_ ''Exam
makeLenses_ ''ExamOccurrence
makeLenses_ ''ExamOfficeLabel
makePrisms ''AuthenticationMode
makeLenses_ ''CourseUserNote

View File

@ -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

View File

@ -1,6 +1,6 @@
$newline never
<h1>
^{nameWidget (userDisplayName user) (userSurname user)}
^{nameWidget userDisplayName userSurname}
<table .table .table--striped .table--hover>
<thead>

View File

@ -0,0 +1,6 @@
$newline never
<td colspan=3>
#{csrf}
^{fvWidget addView}
<td>
^{fvWidget submitView}

View File

@ -0,0 +1,8 @@
$newline never
<td .table__td>
^{labelIdent}
<td .table__td>
#{csrf}
^{fvWidget statusView}
<td .table__td>
^{fvWidget priorityView}

View File

@ -0,0 +1,17 @@
$newline never
<table .table .table--striped .table--hover>
<thead>
<tr .table__row .table__row--head>
<th .table__th>_{MsgTableExamOfficeLabel}
<th .table__th>_{MsgTableExamOfficeLabelStatus}
<th .table__th>_{MsgTableExamOfficeLabelPriority}
<td>
<tbody>
$forall coord <- review liveCoords lLength
<tr .massinput__cell .table__row>
^{cellWdgts ! coord}
<td>
^{fvWidget (delButtons ! coord)}
<tfoot>
<tr .massinput__cell.massinput__cell--add>
^{addWdgets ! (0, 0)}

View File

@ -0,0 +1,4 @@
$newline never
<div .exam-office-label .#{toPathPiece examOfficeLabelStatus}>
#{examOfficeLabelName}

View File

@ -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

View File

@ -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

View File

@ -128,6 +128,8 @@ instance Arbitrary User where
userNotificationSettings <- arbitrary
userCsvOptions <- arbitrary
userShowSex <- arbitrary
userExamOfficeGetSynced <- arbitrary
userExamOfficeGetLabels <- arbitrary
userCreated <- arbitrary
userLastLdapSynchronisation <- arbitrary

View File

@ -37,6 +37,8 @@ fakeUser adjUser = adjUser User{..}
userLanguages = Nothing
userWarningDays = userDefaultWarningDays
userCsvOptions = def
userExamOfficeGetSynced = True
userExamOfficeGetLabels = True
userSex = Nothing
userShowSex = userDefaultShowSex
userNotificationSettings = def