feat(exams): allow mixed ExamGradingMode

This commit is contained in:
Gregor Kleen 2020-02-18 20:11:46 +01:00
parent 0e49bc14e5
commit acffe04350
19 changed files with 251 additions and 75 deletions

View File

@ -49,6 +49,7 @@ export class ExamCorrect {
_partInputs; _partInputs;
_resultSelect; _resultSelect;
_resultGradeSelect; _resultGradeSelect;
_resultPassSelect;
_partDeleteBoxes; _partDeleteBoxes;
_dateFormat; _dateFormat;
@ -80,8 +81,9 @@ export class ExamCorrect {
this._partInputs = [...this._element.querySelectorAll(`input[${EXAM_CORRECT_PART_INPUT_ATTR}]`)]; this._partInputs = [...this._element.querySelectorAll(`input[${EXAM_CORRECT_PART_INPUT_ATTR}]`)];
const resultCell = document.getElementById('uw-exam-correct__result'); const resultCell = document.getElementById('uw-exam-correct__result');
this._resultSelect = resultCell && resultCell.querySelector('select'); this._resultSelect = resultCell && resultCell.querySelector('select');
const resultGradeCell = document.getElementById('uw-exam-correct__result__grade'); const resultDetailCell = document.getElementById('uw-exam-correct__result__pass-grade');
this._resultGradeSelect = resultGradeCell && resultGradeCell.querySelector('select'); this._resultGradeSelect = resultDetailCell && resultDetailCell.querySelector('select.uw-exam-correct__grade');
this._resultPassSelect = resultDetailCell && resultDetailCell.querySelector('select.uw-exam-correct__pass');
this._partDeleteBoxes = [...this._element.querySelectorAll('input.uw-exam-correct--delete-exam-part')]; this._partDeleteBoxes = [...this._element.querySelectorAll('input.uw-exam-correct--delete-exam-part')];
if (this._sendBtn) if (this._sendBtn)
@ -118,15 +120,26 @@ export class ExamCorrect {
if (this._resultSelect && this._resultGradeSelect) { if (this._resultSelect && this._resultGradeSelect) {
this._resultSelect.addEventListener('change', () => { this._resultSelect.addEventListener('change', () => {
if (this._resultSelect.value !== 'attended') if (this._resultSelect.value !== 'grade')
this._resultGradeSelect.classList.add('grade-hidden'); this._resultGradeSelect.classList.add('grade-hidden');
else else
this._resultGradeSelect.classList.remove('grade-hidden'); this._resultGradeSelect.classList.remove('grade-hidden');
}); });
if (this._resultSelect.value !== 'attended') if (this._resultSelect.value !== 'grade')
this._resultGradeSelect.classList.add('grade-hidden'); this._resultGradeSelect.classList.add('grade-hidden');
} }
if (this._resultSelect && this._resultPassSelect) {
this._resultSelect.addEventListener('change', () => {
if (this._resultSelect.value !== 'pass')
this._resultPassSelect.classList.add('pass-hidden');
else
this._resultPassSelect.classList.remove('pass-hidden');
});
if (this._resultSelect.value !== 'pass')
this._resultPassSelect.classList.add('pass-hidden');
}
this._lastColumnIndex = this._element.querySelector('thead > tr').querySelectorAll('th').length - 1; this._lastColumnIndex = this._element.querySelector('thead > tr').querySelectorAll('th').length - 1;
@ -247,12 +260,11 @@ export class ExamCorrect {
case 'delete': case 'delete':
result = null; result = null;
break; break;
case 'passed': case 'pass':
case 'failed': result = { status: 'attended', result: this._resultPassSelect.value };
result = { status: 'attended', result: { Left: this._resultSelect.value } };
break; break;
case 'attended': case 'grade':
result = { status: 'attended', result: { Right: this._resultGradeSelect.value } }; result = { status: 'attended', result: this._resultGradeSelect.value };
break; break;
default: default:
result = { status: this._resultSelect.value }; result = { status: this._resultSelect.value };
@ -457,7 +469,7 @@ export class ExamCorrect {
if (examResult) { if (examResult) {
if (examResult.status === 'attended') if (examResult.status === 'attended')
html = examResult.result.Left || examResult.result.Right; html = examResult.result;
else else
html = examResult.status; html = examResult.status;
} else if (examResult === null) { } else if (examResult === null) {

View File

@ -40,7 +40,7 @@ table[uw-exam-correct]
option option
width: max-content width: max-content
min-width: max-content min-width: max-content
td#uw-exam-correct__result__grade td#uw-exam-correct__result__pass-grade
width: min-content width: min-content
select select
width: max-content width: max-content
@ -49,8 +49,9 @@ table[uw-exam-correct]
width: max-content width: max-content
min-width: max-content min-width: max-content
td#uw-exam-correct__result__grade select.grade-hidden td#uw-exam-correct__result__pass-grade
visibility: hidden select.grade-hidden, select.pass-hidden
visibility: hidden
td.exam-correct--status-cell td.exam-correct--status-cell
font-size: .9rem font-size: .9rem
font-weight: 600 font-weight: 600

View File

@ -1564,8 +1564,8 @@ ExamFinishedParticipant: Bewertung voraussichtlich abgeschlossen
ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden
ExamClosed: Noten gemeldet ExamClosed: Noten gemeldet
ExamClosedTip: Prüfungsbeauftraget, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert ExamClosedTip: Prüfungsbeauftraget, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert
ExamShowGrades: Klausur ist benotet ExamGradingMode: Bewertungsmodus
ExamShowGradesTip: Sollen genaue Noten angezeigt werden, oder sollen Teilnehmer und Prüfungsbeauftragte nur informiert werden, ob die Klausur bestanden wurde? ExamGradingModeTip: In welcher Form werden Prüfungsleistungen für diese Prüfung eingetragen?
ExamPublicStatistics: Statistik veröffentlichen ExamPublicStatistics: Statistik veröffentlichen
ExamPublicStatisticsTip: Soll die automatisch berechnete statistische Auswertung auch den Teilnehmern angezeigt werden, sobald diese ihre Noten einsehen können? ExamPublicStatisticsTip: Soll die automatisch berechnete statistische Auswertung auch den Teilnehmern angezeigt werden, sobald diese ihre Noten einsehen können?
ExamAutomaticGrading: Automatische Notenberechnung ExamAutomaticGrading: Automatische Notenberechnung
@ -1834,6 +1834,7 @@ ExamUserCsvCourseNoteDeleted: Notiz wird gelöscht
ExamUserCsvExceptionNoMatchingUser: Kursteilnehmer konnte nicht eindeutig identifiziert werden ExamUserCsvExceptionNoMatchingUser: Kursteilnehmer konnte nicht eindeutig identifiziert werden
ExamUserCsvExceptionNoMatchingStudyFeatures: Das angegebene Studienfach konnte keinem Studienfach des Kursteilnehmers zugeordnet werden ExamUserCsvExceptionNoMatchingStudyFeatures: Das angegebene Studienfach konnte keinem Studienfach des Kursteilnehmers zugeordnet werden
ExamUserCsvExceptionNoMatchingOccurrence: Raum/Termin konnte nicht eindeutig identifiziert werden ExamUserCsvExceptionNoMatchingOccurrence: Raum/Termin konnte nicht eindeutig identifiziert werden
ExamUserCsvExceptionMismatchedGradingMode expectedGradingMode@ExamGradingMode actualGradingMode@ExamGradingMode: Es wurde versucht eine Prüfungsleistung einzutragen, die zwar vom System interpretiert werden konnte, aber nicht dem für diese Prüfung erwarteten Format entspricht. Das erwartete Format kann unter "Prüfung bearbeiten" angepasst werden ("Bestanden/Nicht Bestanden", "Numerische Noten" oder "Gemischt").
ExternalExamUserCsvRegister: Prüfungsleistung hinterlegen ExternalExamUserCsvRegister: Prüfungsleistung hinterlegen
ExternalExamUserCsvSetTime: Zeitpunkt anpassen ExternalExamUserCsvSetTime: Zeitpunkt anpassen
@ -1854,6 +1855,8 @@ TableHeadingCsvImport: CSV-Import
TableHeadingCsvExport: CSV-Export TableHeadingCsvExport: CSV-Export
ExamResultAttended: Teilgenommen ExamResultAttended: Teilgenommen
ExamResultGrade: Note
ExamResultPass: Bestanden/Nicht Bestanden
ExamResultNoShow: Nicht erschienen ExamResultNoShow: Nicht erschienen
ExamResultVoided: Entwertet ExamResultVoided: Entwertet
ExamResultNone: Kein Prüfungsergebnis ExamResultNone: Kein Prüfungsergebnis
@ -2295,8 +2298,6 @@ ExternalExamExamNamePlaceholder: Klausur, Nachklausur, Projektabnahme, ...
ExternalExamDefaultTime: Voreingestellter Zeitpunkt ExternalExamDefaultTime: Voreingestellter Zeitpunkt
ExternalExamDefaultTimePlaceholder: Zeitpunkt ExternalExamDefaultTimePlaceholder: Zeitpunkt
ExternalExamDefaultTimeTip: Der Zeitpunkt zu dem die Prüfung abgelegt wurde, muss pro Teilnehmer festgelegt werden. Der hier angegebene Zeitpunkt wird als Standardwert für Teilnehmer verwendet, bei denen später nicht ein abweichender Zeitpunkt angegeben wird. ExternalExamDefaultTimeTip: Der Zeitpunkt zu dem die Prüfung abgelegt wurde, muss pro Teilnehmer festgelegt werden. Der hier angegebene Zeitpunkt wird als Standardwert für Teilnehmer verwendet, bei denen später nicht ein abweichender Zeitpunkt angegeben wird.
ExternalExamShowGrades: Klausur ist benotet
ExternalExamShowGradesTip: Sollen genaue Noten angezeigt werden, oder sollen Teilnehmer und Prüfungsbeauftragte nur informiert werden, ob die Klausur bestanden wurde?
ExternalExamExamOfficeSchools: Zusätzliche Institute ExternalExamExamOfficeSchools: Zusätzliche Institute
ExternalExamExamOfficeSchoolsTip: Prüfungsbeauftragte von Instituten, die Sie hier angeben, erhalten im System (zusätzlich zum angegebenen primären Institut) volle Einsicht in sämtliche für diese Prüfung hinterlegten Leistungen, unabhängig von den Studiendaten der Teilnehmer. ExternalExamExamOfficeSchoolsTip: Prüfungsbeauftragte von Instituten, die Sie hier angeben, erhalten im System (zusätzlich zum angegebenen primären Institut) volle Einsicht in sämtliche für diese Prüfung hinterlegten Leistungen, unabhängig von den Studiendaten der Teilnehmer.
ExternalExamStaff: Assoziierte Personen ExternalExamStaff: Assoziierte Personen
@ -2322,4 +2323,8 @@ ExamRoomMappingSurname: Nachnamen beginnend mit
ExamRoomMappingMatriculation: Matrikelnummern endend in ExamRoomMappingMatriculation: Matrikelnummern endend in
ExamRoomLoad: Auslastung ExamRoomLoad: Auslastung
NoFilter: Keine Einschränkung NoFilter: Keine Einschränkung
ExamGradingPass: Bestanden/Nicht Bestanden
ExamGradingGrades: Numerische Noten
ExamGradingMixed: Gemischt

View File

@ -1563,8 +1563,6 @@ ExamFinishedParticipant: Marking expected to be finished
ExamFinishedTip: At this participants are informed of their exam achievements ExamFinishedTip: At this participants are informed of their exam achievements
ExamClosed: Exam achievements registered ExamClosed: Exam achievements registered
ExamClosedTip: At this time exam offices, which pull exam achievements from Uni2work, are informed. Changes to exam achievements trigger further notifications ExamClosedTip: At this time exam offices, which pull exam achievements from Uni2work, are informed. Changes to exam achievements trigger further notifications
ExamShowGrades: Exam is graded
ExamShowGradesTip: Should participants and relevant exam offices be show exact grades or only whether the exam was passed or failed?
ExamPublicStatistics: Publish statistics ExamPublicStatistics: Publish statistics
ExamPublicStatisticsTip: Should automatically computed statistics also be shown to participants as soon as they are informed of their achievements? ExamPublicStatisticsTip: Should automatically computed statistics also be shown to participants as soon as they are informed of their achievements?
ExamAutomaticGrading: Automatically compute grades ExamAutomaticGrading: Automatically compute grades
@ -2288,8 +2286,6 @@ ExternalExamExamNamePlaceholder: Exam, Exam resit, Project discussion, ...
ExternalExamDefaultTime: Default time ExternalExamDefaultTime: Default time
ExternalExamDefaultTimePlaceholder: Time ExternalExamDefaultTimePlaceholder: Time
ExternalExamDefaultTimeTip: The time of the exam needs to be specified for each participant. The time entered here is used as a default value for participants for whom no different time is later specified. ExternalExamDefaultTimeTip: The time of the exam needs to be specified for each participant. The time entered here is used as a default value for participants for whom no different time is later specified.
ExternalExamShowGrades: Exam is graded
ExternalExamShowGradesTip: Should participants and relevant exam offices be show exact grades or only whether the exam was passed or failed?
ExternalExamExamOfficeSchools: Additional departments ExternalExamExamOfficeSchools: Additional departments
ExternalExamExamOfficeSchoolsTip: Exam offices of departments you specify here will also have full access to all results for this exam disregarding the individual participants' features of study. ExternalExamExamOfficeSchoolsTip: Exam offices of departments you specify here will also have full access to all results for this exam disregarding the individual participants' features of study.
ExternalExamStaff: Associated persons ExternalExamStaff: Associated persons

View File

@ -15,7 +15,7 @@ Exam
finished UTCTime Maybe -- Grades shown to students, `ExamCorrector`s locked out finished UTCTime Maybe -- Grades shown to students, `ExamCorrector`s locked out
closed UTCTime Maybe -- Prüfungsamt hat Einsicht (notification) closed UTCTime Maybe -- Prüfungsamt hat Einsicht (notification)
publicStatistics Bool publicStatistics Bool
showGrades Bool gradingMode ExamGradingMode
description Html Maybe description Html Maybe
UniqueExam course name UniqueExam course name
ExamPart ExamPart

View File

@ -4,7 +4,7 @@ ExternalExam
courseName (CI Text) courseName (CI Text)
examName (CI Text) examName (CI Text)
defaultTime UTCTime Maybe defaultTime UTCTime Maybe
showGrades Bool gradingMode ExamGradingMode
UniqueExternalExam term school courseName examName UniqueExternalExam term school courseName examName
ExternalExamResult ExternalExamResult
user UserId user UserId

View File

@ -195,6 +195,7 @@ embedRenderMessage ''UniWorX ''CsvPreset id
embedRenderMessage ''UniWorX ''Quoting ("Csv" <>) embedRenderMessage ''UniWorX ''Quoting ("Csv" <>)
embedRenderMessage ''UniWorX ''FavouriteReason id embedRenderMessage ''UniWorX ''FavouriteReason id
embedRenderMessage ''UniWorX ''Sex id embedRenderMessage ''UniWorX ''Sex id
embedRenderMessage ''UniWorX ''ExamGradingMode id
embedRenderMessage ''UniWorX ''AuthenticationMode id embedRenderMessage ''UniWorX ''AuthenticationMode id

View File

@ -46,7 +46,7 @@ postEEditR tid ssh csh examn = do
, examFinished = efFinished , examFinished = efFinished
, examClosed = examClosed oldExam , examClosed = examClosed oldExam
, examPublicStatistics = efPublicStatistics , examPublicStatistics = efPublicStatistics
, examShowGrades = efShowGrades , examGradingMode = efGradingMode
, examDescription = efDescription , examDescription = efDescription
} }

View File

@ -26,7 +26,7 @@ import Text.Blaze.Html.Renderer.String (renderHtml)
data ExamForm = ExamForm data ExamForm = ExamForm
{ efName :: ExamName { efName :: ExamName
, efDescription :: Maybe Html , efDescription :: Maybe Html
, efShowGrades :: Bool , efGradingMode :: ExamGradingMode
, efStart :: Maybe UTCTime , efStart :: Maybe UTCTime
, efEnd :: Maybe UTCTime , efEnd :: Maybe UTCTime
, efVisibleFrom :: Maybe UTCTime , efVisibleFrom :: Maybe UTCTime
@ -80,7 +80,7 @@ examForm template html = do
flip (renderAForm FormStandard) html $ ExamForm flip (renderAForm FormStandard) html $ ExamForm
<$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template) <$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template)
<*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslpI MsgExamDescription "Html") (efDescription <$> template)) <*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslpI MsgExamDescription "Html") (efDescription <$> template))
<*> apopt checkBoxField (fslI MsgExamShowGrades & setTooltip MsgExamShowGradesTip) (efShowGrades <$> template <|> Just True) <*> apopt (selectField optionsFinite) (fslI MsgExamGradingMode & setTooltip MsgExamGradingModeTip) (efGradingMode <$> template <|> Just ExamGradingMixed)
<* aformSection MsgExamFormTimes <* aformSection MsgExamFormTimes
<*> aopt utcTimeField (fslpI MsgExamStart (mr MsgDate) & setTooltip MsgExamTimeTip) (efStart <$> template) <*> aopt utcTimeField (fslpI MsgExamStart (mr MsgDate) & setTooltip MsgExamTimeTip) (efStart <$> template)
<*> aopt utcTimeField (fslpI MsgExamEnd (mr MsgDate) & setTooltip MsgExamTimeTip) (efEnd <$> template) <*> aopt utcTimeField (fslpI MsgExamEnd (mr MsgDate) & setTooltip MsgExamTimeTip) (efEnd <$> template)
@ -253,7 +253,7 @@ examFormTemplate (Entity eId Exam{..}) = do
, efStart = examStart , efStart = examStart
, efEnd = examEnd , efEnd = examEnd
, efFinished = examFinished , efFinished = examFinished
, efShowGrades = examShowGrades , efGradingMode = examGradingMode
, efPublicStatistics = examPublicStatistics , efPublicStatistics = examPublicStatistics
, efDescription = examDescription , efDescription = examDescription
, efOccurrences = Set.fromList $ do , efOccurrences = Set.fromList $ do
@ -321,7 +321,7 @@ examTemplate cid = runMaybeT $ do
, efStart = dateOffset <$> examStart oldExam , efStart = dateOffset <$> examStart oldExam
, efEnd = dateOffset <$> examEnd oldExam , efEnd = dateOffset <$> examEnd oldExam
, efFinished = dateOffset <$> examFinished oldExam , efFinished = dateOffset <$> examFinished oldExam
, efShowGrades = examShowGrades oldExam , efGradingMode = examGradingMode oldExam
, efPublicStatistics = examPublicStatistics oldExam , efPublicStatistics = examPublicStatistics oldExam
, efDescription = examDescription oldExam , efDescription = examDescription oldExam
, efOccurrences = Set.empty , efOccurrences = Set.empty

View File

@ -42,7 +42,7 @@ postCExamNewR tid ssh csh = do
, examEnd = efEnd , examEnd = efEnd
, examFinished = efFinished , examFinished = efFinished
, examClosed = Nothing , examClosed = Nothing
, examShowGrades = efShowGrades , examGradingMode = efGradingMode
, examPublicStatistics = efPublicStatistics , examPublicStatistics = efPublicStatistics
, examDescription = efDescription , examDescription = efDescription
} }

View File

@ -172,7 +172,7 @@ resultAutomaticExamResult exam@Exam{..} examBonus' = folding . runReader $ do
parts' <- asks $ sequence . toListOf (resultExamPartResults . to (^? _Just . _entityVal . _examPartResultResult)) parts' <- asks $ sequence . toListOf (resultExamPartResults . to (^? _Just . _entityVal . _examPartResultResult))
bonus <- preview $ resultExamBonus . _entityVal . _examBonusBonus <> resultAutomaticExamBonus exam examBonus' bonus <- preview $ resultExamBonus . _entityVal . _examBonusBonus <> resultAutomaticExamBonus exam examBonus'
let gradeRes = examGrade exam bonus =<< parts' let gradeRes = examGrade exam bonus =<< parts'
return $ fmap (bool (Left . view passingGrade) Right examShowGrades) <$> gradeRes return $ fmap (bool Right (Left . view passingGrade) $ is _ExamGradingPass examGradingMode) <$> gradeRes
csvExamPartHeader :: Prism' Csv.Name ExamPartNumber csvExamPartHeader :: Prism' Csv.Name ExamPartNumber
@ -391,6 +391,7 @@ data ExamUserCsvException
= ExamUserCsvExceptionNoMatchingUser = ExamUserCsvExceptionNoMatchingUser
| ExamUserCsvExceptionNoMatchingStudyFeatures | ExamUserCsvExceptionNoMatchingStudyFeatures
| ExamUserCsvExceptionNoMatchingOccurrence | ExamUserCsvExceptionNoMatchingOccurrence
| ExamUserCsvExceptionMismatchedGradingMode ExamGradingMode ExamGradingMode
deriving (Show, Generic, Typeable) deriving (Show, Generic, Typeable)
instance Exception ExamUserCsvException instance Exception ExamUserCsvException
@ -587,7 +588,7 @@ postEUsersR tid ssh csh examn = do
<$> aopt pointsField (fslI MsgPoints) Nothing <$> aopt pointsField (fslI MsgPoints) Nothing
, singletonMap ExamUserSetResult $ , singletonMap ExamUserSetResult $
ExamUserSetResultData ExamUserSetResultData
<$> aopt (examResultField (Just $ SomeMessage MsgExamResultNone) examPassedGradeField) (fslI MsgExamResult) Nothing <$> aopt (examResultModeField (Just $ SomeMessage MsgExamResultNone) examGradingMode) (fslI MsgExamResult) Nothing
] ]
actionOpts :: Handler (OptionList ExamUserAction) actionOpts :: Handler (OptionList ExamUserAction)
@ -663,8 +664,9 @@ postEUsersR tid ssh csh examn = do
when (doBonus && is _Just (join $ csvEUserBonus dbCsvNew)) $ when (doBonus && is _Just (join $ csvEUserBonus dbCsvNew)) $
yield . ExamUserCsvSetBonusData False uid . join $ csvEUserBonus dbCsvNew yield . ExamUserCsvSetBonusData False uid . join $ csvEUserBonus dbCsvNew
when (is _Just $ csvEUserExamResult dbCsvNew) $ whenIsJust (csvEUserExamResult dbCsvNew) $ \res -> do
yield . ExamUserCsvSetResultData False uid $ csvEUserExamResult dbCsvNew yield . ExamUserCsvSetResultData False uid $ csvEUserExamResult dbCsvNew
guardResultKind res
note <- lift . getBy $ UniqueCourseUserNote uid examCourse note <- lift . getBy $ UniqueCourseUserNote uid examCourse
when (csvEUserCourseNote dbCsvNew /= note ^? _Just . _entityVal . _courseUserNoteNote) $ when (csvEUserCourseNote dbCsvNew /= note ^? _Just . _entityVal . _courseUserNoteNote) $
@ -696,7 +698,7 @@ postEUsersR tid ssh csh examn = do
oldBonus = dbCsvOld ^? (resultExamBonus . _entityVal . _examBonusBonus <> resultAutomaticExamBonus') oldBonus = dbCsvOld ^? (resultExamBonus . _entityVal . _examBonusBonus <> resultAutomaticExamBonus')
newResult, oldResult :: Maybe ExamResultPassedGrade newResult, oldResult :: Maybe ExamResultPassedGrade
newResult = fmap (fmap $ bool (Left . view passingGrade) Right examShowGrades) . examGrade examVal (newBonus <|> oldBonus) =<< newResults newResult = fmap (fmap $ bool Right (Left . view passingGrade) $ is _ExamGradingGrades examGradingMode) . examGrade examVal (newBonus <|> oldBonus) =<< newResults
oldResult = dbCsvOld ^? (resultExamResult . _entityVal . _examResultResult <> resultAutomaticExamResult') oldResult = dbCsvOld ^? (resultExamResult . _entityVal . _examResultResult <> resultAutomaticExamResult')
when doBonus $ when doBonus $
@ -717,13 +719,16 @@ postEUsersR tid ssh csh examn = do
-> return () -> return ()
_ | is _Nothing $ csvEUserExamResult dbCsvNew _ | is _Nothing $ csvEUserExamResult dbCsvNew
-> return () -> return ()
Nothing Nothing -> do
-> yield . ExamUserCsvSetResultData False uid $ csvEUserExamResult dbCsvNew yield . ExamUserCsvSetResultData False uid $ csvEUserExamResult dbCsvNew
whenIsJust (csvEUserExamResult dbCsvNew) guardResultKind
Just _ Just _
| csvEUserExamResult dbCsvNew /= newResult | csvEUserExamResult dbCsvNew /= newResult -> do
-> yield . ExamUserCsvSetResultData True uid $ csvEUserExamResult dbCsvNew yield . ExamUserCsvSetResultData True uid $ csvEUserExamResult dbCsvNew
| oldResult /= newResult whenIsJust (csvEUserExamResult dbCsvNew) guardResultKind
-> yield . ExamUserCsvSetResultData False uid $ csvEUserExamResult dbCsvNew | oldResult /= newResult -> do
yield . ExamUserCsvSetResultData False uid $ csvEUserExamResult dbCsvNew
whenIsJust (csvEUserExamResult dbCsvNew) guardResultKind
| otherwise | otherwise
-> return () -> return ()
@ -938,6 +943,19 @@ postEUsersR tid ssh csh examn = do
, dbtCsvRenderException = ap getMessageRender . pure :: ExamUserCsvException -> DB Text , dbtCsvRenderException = ap getMessageRender . pure :: ExamUserCsvException -> DB Text
} }
where where
guardResultKind :: MonadThrow m => ExamResultPassedGrade -> m ()
guardResultKind res
| ( is _ExamGradingPass examGradingMode
&& is (_ExamAttended . _Right) res
) ||
( is _ExamGradingGrades examGradingMode
&& is (_ExamAttended . _Left) res
)
= throwM . ExamUserCsvExceptionMismatchedGradingMode examGradingMode $ if
| is (_ExamAttended . _Left) res -> ExamGradingPass
| otherwise -> ExamGradingGrades
| otherwise = return ()
registeredUserName :: Map (E.Value ExamRegistrationId) ExamUserTableData -> ExamRegistrationId -> Widget registeredUserName :: Map (E.Value ExamRegistrationId) ExamUserTableData -> ExamRegistrationId -> Widget
registeredUserName existing (E.Value -> registration) = nameWidget userDisplayName userSurname registeredUserName existing (E.Value -> registration) = nameWidget userDisplayName userSurname
where where

View File

@ -35,7 +35,7 @@ postEEEditR tid ssh coursen examn = do
, eefCourseName = coursen , eefCourseName = coursen
, eefExamName = examn , eefExamName = examn
, eefDefaultTime = externalExamDefaultTime , eefDefaultTime = externalExamDefaultTime
, eefShowGrades = externalExamShowGrades , eefGradingMode = externalExamGradingMode
, eefOfficeSchools = schools , eefOfficeSchools = schools
, eefStaff = staff , eefStaff = staff
} }
@ -50,7 +50,7 @@ postEEEditR tid ssh coursen examn = do
, externalExamCourseName = eefCourseName , externalExamCourseName = eefCourseName
, externalExamExamName = eefExamName , externalExamExamName = eefExamName
, externalExamDefaultTime = eefDefaultTime , externalExamDefaultTime = eefDefaultTime
, externalExamShowGrades = eefShowGrades , externalExamGradingMode = eefGradingMode
} }
when (is _Nothing replaceRes) $ do when (is _Nothing replaceRes) $ do
audit $ TransactionExternalExamEdit eeId audit $ TransactionExternalExamEdit eeId

View File

@ -20,7 +20,7 @@ data ExternalExamForm = ExternalExamForm
, eefCourseName :: CI Text , eefCourseName :: CI Text
, eefExamName :: CI Text , eefExamName :: CI Text
, eefDefaultTime :: Maybe UTCTime , eefDefaultTime :: Maybe UTCTime
, eefShowGrades :: Bool , eefGradingMode :: ExamGradingMode
, eefOfficeSchools :: Set SchoolId , eefOfficeSchools :: Set SchoolId
, eefStaff :: Set (Either UserEmail UserId) , eefStaff :: Set (Either UserEmail UserId)
} }
@ -51,7 +51,7 @@ externalExamForm template = validateForm validateExternalExam $ \html -> do
<*> areq (textField & cfStrip & cfCI) (fslI MsgExternalExamCourseName & setTooltip MsgExternalExamCourseNameTip & addPlaceholder (mr MsgExternalExamCourseNamePlaceholder)) (eefCourseName <$> template) <*> areq (textField & cfStrip & cfCI) (fslI MsgExternalExamCourseName & setTooltip MsgExternalExamCourseNameTip & addPlaceholder (mr MsgExternalExamCourseNamePlaceholder)) (eefCourseName <$> template)
<*> areq (textField & cfStrip & cfCI) (fslI MsgExternalExamExamName & setTooltip MsgExternalExamExamNameTip & addPlaceholder (mr MsgExternalExamExamNamePlaceholder)) (eefExamName <$> template) <*> areq (textField & cfStrip & cfCI) (fslI MsgExternalExamExamName & setTooltip MsgExternalExamExamNameTip & addPlaceholder (mr MsgExternalExamExamNamePlaceholder)) (eefExamName <$> template)
<*> aopt utcTimeField (fslI MsgExternalExamDefaultTime & setTooltip MsgExternalExamDefaultTimeTip & addPlaceholder (mr MsgExternalExamDefaultTimePlaceholder)) (eefDefaultTime <$> template) <*> aopt utcTimeField (fslI MsgExternalExamDefaultTime & setTooltip MsgExternalExamDefaultTimeTip & addPlaceholder (mr MsgExternalExamDefaultTimePlaceholder)) (eefDefaultTime <$> template)
<*> apopt checkBoxField (fslI MsgExternalExamShowGrades & setTooltip MsgExternalExamShowGradesTip) (eefShowGrades <$> template) <*> apopt (selectField optionsFinite) (fslI MsgExamGradingMode & setTooltip MsgExamGradingModeTip) (eefGradingMode <$> template <|> Just ExamGradingMixed)
<*> (Set.fromList <$> officeSchoolForm cRoute (Set.toList . eefOfficeSchools <$> template)) <*> (Set.fromList <$> officeSchoolForm cRoute (Set.toList . eefOfficeSchools <$> template))
<*> (Set.fromList <$> staffForm cRoute ((Set.toList . eefStaff <$> template) <|> pure (pure $ Right uid))) <*> (Set.fromList <$> staffForm cRoute ((Set.toList . eefStaff <$> template) <|> pure (pure $ Right uid)))
where where

View File

@ -28,7 +28,7 @@ postEExamNewR = do
, externalExamCourseName = eefCourseName , externalExamCourseName = eefCourseName
, externalExamExamName = eefExamName , externalExamExamName = eefExamName
, externalExamDefaultTime = eefDefaultTime , externalExamDefaultTime = eefDefaultTime
, externalExamShowGrades = eefShowGrades , externalExamGradingMode = eefGradingMode
} }
whenIsJust insertRes $ \eeId -> do whenIsJust insertRes $ \eeId -> do
audit $ TransactionExternalExamEdit eeId audit $ TransactionExternalExamEdit eeId

View File

@ -1230,21 +1230,30 @@ examResultField :: forall m res.
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
, PathPiece res , PathPiece res
) )
=> Maybe (SomeMessage UniWorX) -> Field m res -> Field m (ExamResult' res) => Maybe (SomeMessage UniWorX) -> Handler (OptionList (Either Text res -> Bool, Field m res)) -> Field m (ExamResult' res)
examResultField optMsg innerField = Field examResultField optMsg mkOl = Field
{ fieldEnctype = UrlEncoded <> fieldEnctype innerField { fieldEnctype = UrlEncoded -- breaks if mkOl contains options with other enctype
, fieldParse = \ts fs -> if , fieldParse = \ts fs -> do
| res : _ <- mapMaybe (assertM ((||) <$> is _ExamNoShow <*> is _ExamVoided) . fromPathPiece) ts ol@OptionList{..} <- liftHandler mkOl
-> return . Right $ Just res if
| null ts || any null ts | res : _ <- mapMaybe (assertM ((||) <$> is _ExamNoShow <*> is _ExamVoided) . fromPathPiece) ts
-> return $ Right Nothing -> return . Right $ Just res
| otherwise | any null ts
-> fmap (fmap ExamAttended) <$> fieldParse innerField (filter (not . (`elem` ["", "attended", "no-show", "voided"])) ts) fs -> return $ Right Nothing
| (optPred, innerField) : _ <- mapMaybe olReadExternal ts
-> fmap (fmap ExamAttended) <$> fieldParse innerField (filter (`notElem` outerOptions ol) $ filter (optPred . Left) ts) fs
| [] <- ts
-> return $ Right Nothing
| t : _ <- ts
-> return . Left . SomeMessage $ MsgInvalidEntry t
, fieldView = \theId name attrs val isReq -> do , fieldView = \theId name attrs val isReq -> do
innerId <- newIdent innerId <- newIdent
OptionList{..} <- liftHandler mkOl
let let
innerVal :: Either Text res innerVal :: Either Text res
innerVal = val >>= maybe (Left "") return . preview _ExamAttended innerVal = val >>= maybe (Left "") return . preview _ExamAttended
matchesPred Option{ optionInternalValue = (optPred, _) } = has (_Right . _ExamAttended . filtered (optPred . Right)) val
[whamlet| [whamlet|
$newline never $newline never
<div> <div>
@ -1252,13 +1261,107 @@ examResultField optMsg innerField = Field
$maybe optMsg' <- guardOnM (not isReq) optMsg $maybe optMsg' <- guardOnM (not isReq) optMsg
<option value="" :is _Left val:selected> <option value="" :is _Left val:selected>
_{optMsg'} _{optMsg'}
<option value="attended" :is (_Right . _ExamAttended) val:selected>_{MsgExamResultAttended} $forall opt@Option{optionDisplay, optionExternalValue} <- olOptions
<option value="no-show" :is (_Right . _ExamNoShow) val:selected>_{MsgExamResultNoShow} <option value=#{optionExternalValue} :matchesPred opt:selected>
<option value="voided" :is (_Right . _ExamVoided) val:selected>_{MsgExamResultVoided} #{optionDisplay}
<fieldset uw-interactive-fieldset .interactive-fieldset__target data-conditional-input=#{theId} data-conditional-value="attended" style="display: inline-block"> <option value=#{toPathPiece noShowVal} :is (_Right . _ExamNoShow) val:selected>
^{fieldView innerField innerId name attrs innerVal True} _{MsgExamResultNoShow}
<option value=#{toPathPiece voidedVal} :is (_Right . _ExamVoided) val:selected>
_{MsgExamResultVoided}
$forall Option{..} <- olOptions
<fieldset uw-interactive-fieldset .interactive-fieldset__target data-conditional-input=#{theId} data-conditional-value=#{optionExternalValue} style="display: inline-block">
<legend>
#{optionDisplay}
^{fieldView (snd optionInternalValue) innerId name attrs innerVal True}
|] |]
} }
where
outerOptions OptionList{..} =
[ ""
, toPathPiece noShowVal
, toPathPiece voidedVal
] ++ [ optionExternalValue | Option{..} <- olOptions ]
noShowVal, voidedVal :: ExamResult' res
noShowVal = ExamNoShow
voidedVal = ExamVoided
examResultGradeField :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> Maybe (SomeMessage UniWorX) -> Field m ExamResultGrade
examResultGradeField = flip examResultField $ do
MsgRenderer mr <- getMsgRenderer
return $ mkOptionList
[ Option
{ optionDisplay = mr MsgExamResultGrade
, optionExternalValue = "grade"
, optionInternalValue =
( const True
, examGradeField
)
}
]
examResultPassedField :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> Maybe (SomeMessage UniWorX) -> Field m ExamResultPassed
examResultPassedField = flip examResultField $ do
MsgRenderer mr <- getMsgRenderer
return $ mkOptionList
[ Option
{ optionDisplay = mr MsgExamResultPass
, optionExternalValue = "pass"
, optionInternalValue =
( const True
, examPassedField
)
}
]
examResultPassedGradeField :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> Maybe (SomeMessage UniWorX) -> Field m ExamResultPassedGrade
examResultPassedGradeField = flip examResultField $ do
MsgRenderer mr <- getMsgRenderer
return $ mkOptionList
[ Option
{ optionDisplay = mr MsgExamResultGrade
, optionExternalValue = "grade"
, optionInternalValue =
( either (`elem` map toPathPiece grades) (is _Right)
, hoistField liftHandler . selectField $ fmap Right <$> optionsFinite
)
}
, Option
{ optionDisplay = mr MsgExamResultPass
, optionExternalValue = "pass"
, optionInternalValue =
( either (`elem` map toPathPiece passResults) (is _Left)
, hoistField liftHandler . selectField $ fmap Left <$> optionsFinite
)
}
]
where
grades :: [ExamGrade]
grades = universeF
passResults :: [ExamPassed]
passResults = universeF
examResultModeField :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> Maybe (SomeMessage UniWorX) -> ExamGradingMode -> Field m ExamResultPassedGrade
examResultModeField optMsg ExamGradingGrades = convertField (fmap Right) (fmap $ either (review passingGrade) id) $ examResultGradeField optMsg
examResultModeField optMsg ExamGradingPass = convertField (fmap Left) (fmap $ either id (view passingGrade)) $ examResultPassedField optMsg
examResultModeField optMsg ExamGradingMixed = examResultPassedGradeField optMsg
examGradeField :: forall m. examGradeField :: forall m.
( MonadHandler m ( MonadHandler m
@ -1279,7 +1382,7 @@ examPassedGradeField :: forall m.
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
) )
=> Field m (Either ExamPassed ExamGrade) => Field m (Either ExamPassed ExamGrade)
examPassedGradeField = hoistField liftHandler . selectField $ (<>) <$> (fmap Left <$> optionsFinite) <*> (fmap Right <$> optionsFinite) examPassedGradeField = hoistField liftHandler . selectField $ (<>) <$> (fmap Right <$> optionsFinite) <*> (fmap Left <$> optionsFinite)
data CsvFormatOptions' = CsvFormatOptionsPreset' CsvPreset data CsvFormatOptions' = CsvFormatOptionsPreset' CsvPreset

View File

@ -286,7 +286,7 @@ fltrExamResultPoints queryExamResult = singletonMap "exam-result" . FilterColumn
fltrExamResultPointsUI :: DBFilterUI fltrExamResultPointsUI :: DBFilterUI
fltrExamResultPointsUI mPrev = prismAForm (singletonFilter "exam-result" . maybePrism _PathPiece) mPrev $ aopt (examResultField (Just $ SomeMessage MsgNoFilter) examPassedGradeField) (fslI MsgExamResult) fltrExamResultPointsUI mPrev = prismAForm (singletonFilter "exam-result" . maybePrism _PathPiece) mPrev $ aopt (examResultPassedGradeField . Just $ SomeMessage MsgNoFilter) (fslI MsgExamResult)
------------- -------------
-- Courses -- -- Courses --

View File

@ -614,6 +614,26 @@ customMigrations = Map.fromListWith (>>)
res' = Left . view passingGrade <$> res res' = Left . view passingGrade <$> res
in [executeQQ|UPDATE exam_result SET result = #{res'} WHERE id = #{resId};|] in [executeQQ|UPDATE exam_result SET result = #{res'} WHERE id = #{resId};|]
) )
, ( AppliedMigrationKey [migrationVersion|31.0.0|] [version|32.0.0|]
, whenM (tableExists "exam") $
[executeQQ|
ALTER TABLE "exam" ADD COLUMN "grading_mode" character varying;
UPDATE "exam" SET "grading_mode" = 'grades' WHERE "show_grades";
UPDATE "exam" SET "grading_mode" = 'pass' WHERE NOT "show_grades";
ALTER TABLE "exam" DROP COLUMN "show_grades";
ALTER TABLE "exam" ALTER COLUMN "grading_mode" SET NOT NULL;
|]
)
, ( AppliedMigrationKey [migrationVersion|32.0.0|] [version|33.0.0|]
, whenM (tableExists "external_exam") $
[executeQQ|
ALTER TABLE "external_exam" ADD COLUMN "grading_mode" character varying;
UPDATE "external_exam" SET "grading_mode" = 'grades' WHERE "show_grades";
UPDATE "external_exam" SET "grading_mode" = 'pass' WHERE NOT "show_grades";
ALTER TABLE "external_exam" DROP COLUMN "show_grades";
ALTER TABLE "external_exam" ALTER COLUMN "grading_mode" SET NOT NULL;
|]
)
] ]

View File

@ -27,12 +27,15 @@ module Model.Types.Exam
, passingGrade , passingGrade
, ExamResultPoints, ExamResultGrade, ExamResultPassed , ExamResultPoints, ExamResultGrade, ExamResultPassed
, ExamResultPassedGrade , ExamResultPassedGrade
, ExamGradingMode(..)
, _ExamGradingPass, _ExamGradingGrades, _ExamGradingMixed
, ExamPartNumber , ExamPartNumber
, _ExamPartNumber, _ExamPartNumber' , _ExamPartNumber, _ExamPartNumber'
) where ) where
import Import.NoModel import Import.NoModel
import Model.Types.Common import Model.Types.Common
import Model.Types.TH.PathPiece
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Map as Map import qualified Data.Map as Map
@ -345,6 +348,21 @@ instance {-# OVERLAPPING #-} FromJSON (Either ExamPassed ExamGrade) where
parseJSON x = (Left <$> parseJSON x) <|> (Right <$> parseJSON x) parseJSON x = (Left <$> parseJSON x) <|> (Right <$> parseJSON x)
data ExamGradingMode
= ExamGradingPass
| ExamGradingGrades
| ExamGradingMixed
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe ExamGradingMode
instance Finite ExamGradingMode
nullaryPathPiece ''ExamGradingMode $ camelToPathPiece' 2
pathPieceJSON ''ExamGradingMode
pathPieceJSONKey ''ExamGradingMode
derivePersistFieldPathPiece ''ExamGradingMode
makePrisms ''ExamGradingMode
newtype ExamPartNumber = ExamPartNumber { examPartNumberFragments :: [Either (CI Text) Natural] } newtype ExamPartNumber = ExamPartNumber { examPartNumberFragments :: [Either (CI Text) Natural] }
deriving (Eq, Ord, Generic, Typeable) deriving (Eq, Ord, Generic, Typeable)

View File

@ -36,25 +36,27 @@ $newline never
<select> <select>
<option value="none"> <option value="none">
_{MsgExamCorrectExamResultNone} _{MsgExamCorrectExamResultNone}
$if examShowGrades $if is _ExamGradingGrades examGradingMode || is _ExamGradingMixed examGradingMode
<option value="attended"> <option value="grade">
_{MsgExamResult} _{MsgExamResultGrade}
$else $if is _ExamGradingPass examGradingMode || is _ExamGradingMixed examGradingMode
<option value="passed"> <option value="pass">
_{MsgExamPassed} _{MsgExamResultPass}
<option value="failed">
_{MsgExamNotPassed}
<option value="voided"> <option value="voided">
_{MsgExamResultVoided} _{MsgExamResultVoided}
<option value="no-show"> <option value="no-show">
_{MsgExamResultNoShow} _{MsgExamResultNoShow}
<option value="delete"> <option value="delete">
_{MsgExamCorrectExamResultDelete} _{MsgExamCorrectExamResultDelete}
<td .table__td #uw-exam-correct__result__grade> <td .table__td #uw-exam-correct__result__pass-grade>
<select> <select .uw-exam-correct__grade>
$forall grade <- (toPathPiece <$> examGrades) $forall grade <- examGrades
<option> <option value=#{toPathPiece grade}>
#{grade} _{grade}
<select .uw-exam-correct__pass>
$forall pass <- [ExamPassed True, ExamPassed False]
<option value=#{toPathPiece pass}>
_{pass}
<td #exam-correct__status .table__td> <td #exam-correct__status .table__td>
<button #exam-correct__send-btn .btn .btn-primary> <button #exam-correct__send-btn .btn .btn-primary>
_{MsgExamCorrectButtonSend} _{MsgExamCorrectButtonSend}