158 lines
7.2 KiB
Haskell
158 lines
7.2 KiB
Haskell
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
|
--
|
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
module Handler.Utils.Sheet where
|
|
|
|
import Import
|
|
|
|
import qualified Database.Esqueleto.Legacy as E
|
|
import qualified Database.Esqueleto.Internal.Internal as E
|
|
|
|
import qualified Data.Map.Strict as Map
|
|
|
|
import Text.Hamlet
|
|
|
|
|
|
-- | Map sheet file types to their visibily dates of a given sheet, for convenience
|
|
sheetFileTypeDates :: Sheet -> SheetFileType -> Maybe UTCTime
|
|
sheetFileTypeDates Sheet{..} = \case
|
|
SheetExercise -> sheetActiveFrom
|
|
SheetHint -> sheetHintFrom
|
|
SheetSolution -> sheetSolutionFrom
|
|
SheetMarking -> Nothing
|
|
|
|
|
|
fetchSheetAux :: ( E.SqlSelect b a
|
|
, Typeable a, MonadHandler m
|
|
)
|
|
=> (E.SqlExpr (Entity Sheet) -> E.SqlExpr (Entity Course) -> b)
|
|
-> TermId -> SchoolId -> CourseShorthand -> SheetName -> SqlReadT m a
|
|
fetchSheetAux prj tid ssh csh shn =
|
|
let cachId = encodeUtf8 $ tshow (tid, ssh, csh, shn)
|
|
in cachedBy cachId $ do
|
|
-- Mit Yesod:
|
|
-- cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
|
-- getBy404 $ CourseSheet cid shn
|
|
-- Mit Esqueleto:
|
|
sheetList <- E.select . E.from $ \(course `E.InnerJoin` sheet) -> do
|
|
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
|
|
E.where_ $ course E.^. CourseTerm E.==. E.val tid
|
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
|
E.&&. course E.^. CourseShorthand E.==. E.val csh
|
|
E.&&. sheet E.^. SheetName E.==. E.val shn
|
|
return $ prj sheet course
|
|
case sheetList of
|
|
[sheet] -> return sheet
|
|
_other -> notFound
|
|
|
|
fetchSheet :: TermId -> SchoolId -> CourseShorthand -> SheetName -> YesodDB UniWorX (Entity Sheet)
|
|
fetchSheet = fetchSheetAux const
|
|
|
|
fetchSheetCourse :: TermId -> SchoolId -> CourseShorthand -> SheetName -> YesodDB UniWorX (Entity Sheet, Entity Course)
|
|
fetchSheetCourse = fetchSheetAux (,)
|
|
|
|
fetchSheetId :: TermId -> SchoolId -> CourseShorthand -> SheetName -> YesodDB UniWorX (Key Sheet)
|
|
fetchSheetId tid ssh cid shn = E.unValue <$> fetchSheetAux (\sheet _ -> sheet E.^. SheetId) tid ssh cid shn
|
|
|
|
fetchSheetIdCourseId :: TermId -> SchoolId -> CourseShorthand -> SheetName -> YesodDB UniWorX (Key Sheet, Key Course)
|
|
fetchSheetIdCourseId tid ssh cid shn = bimap E.unValue E.unValue <$> fetchSheetAux (\sheet course -> (sheet E.^. SheetId, course E.^. CourseId)) tid ssh cid shn
|
|
|
|
|
|
data ResolveSheetTypeException
|
|
= ResolveSheetTypeExamPartUnavailable SqlBackendKey
|
|
| ResolveSheetTypeForeignExam
|
|
deriving (Eq, Ord, Read, Show, Generic)
|
|
deriving anyclass (Exception)
|
|
|
|
resolveSheetType :: ( MonadThrow m
|
|
, MonadIO m
|
|
)
|
|
=> CourseId
|
|
-> SheetType SqlBackendKey
|
|
-> ReaderT SqlBackend m (SheetType (Entity ExamPart))
|
|
resolveSheetType cId = traverse $ \epId'@(review _SqlKey -> epId) -> do
|
|
ep@(Entity _ ExamPart{..}) <- maybe (throwM $ ResolveSheetTypeExamPartUnavailable epId') return =<< getEntity epId
|
|
Exam{..} <- getJust examPartExam
|
|
if | examCourse /= cId -> throwM ResolveSheetTypeForeignExam
|
|
| otherwise -> return ep
|
|
|
|
resolveSheetTypeRating :: ( MonadThrow m
|
|
, MonadIO m
|
|
)
|
|
=> CourseId
|
|
-> SheetType SqlBackendKey
|
|
-> ReaderT SqlBackend m (SheetType RatingExamPartReference)
|
|
resolveSheetTypeRating cId dbST = do
|
|
eST <- resolveSheetType cId dbST
|
|
case matching _ExamPartPoints eST of
|
|
Left t -> return t
|
|
Right (Entity _ ExamPart{..}, weight, grading) -> do
|
|
Exam{..} <- getJust examPartExam
|
|
return ExamPartPoints
|
|
{ examPart = RatingExamPartReference examName examPartNumber
|
|
, ..
|
|
}
|
|
|
|
sheetTypeDescription :: forall m.
|
|
( MonadHandler m, HandlerSite m ~ UniWorX
|
|
)
|
|
=> CourseId
|
|
-> SheetType SqlBackendKey
|
|
-> ReaderT SqlBackend m (HtmlUrlI18n (SomeMessage UniWorX) (Route UniWorX))
|
|
sheetTypeDescription cId dbST = hoist liftHandler $ do
|
|
sType' <- resolveSheetType cId dbST
|
|
sType <- for sType' $ \(Entity _epId ExamPart{..}) -> do
|
|
Exam{..} <- getJust examPartExam
|
|
Course{..} <- getJust examCourse
|
|
cTime <- liftIO getCurrentTime
|
|
lecturerInfo <- hasReadAccessTo $ CExamR courseTerm courseSchool courseShorthand examName EEditR
|
|
let partVisible = gradingVisible
|
|
|| NTop (Just cTime) >= NTop examPartsFrom
|
|
|| lecturerInfo
|
|
gradingVisible = NTop (Just cTime) >= NTop examPartsFrom
|
|
|| lecturerInfo
|
|
return (examName, examPartName, examPartNumber, partVisible, gradingVisible, CExamR courseTerm courseSchool courseShorthand examName EShowR)
|
|
return $(ihamletFile "templates/widgets/sheetType.hamlet")
|
|
|
|
|
|
|
|
sheetExamResult :: SheetTypeSummary ExamPartId -> Entity ExamPart -> Maybe ExamResultPoints
|
|
sheetExamResult SheetTypeSummary{ examSummary = MergeMap examSummary'' } (Entity epId ExamPart{..}) = Map.lookup epId examSummary'' <&> \examSummary' ->
|
|
let
|
|
sumOfWeights = getSum $ foldMap (views _1 Sum) examSummary'
|
|
weightRescale = recip sumOfWeights
|
|
|
|
toExamPoints :: (Rational, SheetGradeSummary) -> Maybe Rational
|
|
toExamPoints (weight, summary)
|
|
| sumOfWeights <= 0 = Nothing
|
|
| otherwise = Just . (* weight) $ case examPartMaxPoints of
|
|
Just maxPoints -> toRational maxPoints * bonusProp
|
|
Nothing -> bonusProp * possible
|
|
where
|
|
bonusProp :: Rational
|
|
bonusProp | possible <= 0 = 1
|
|
| otherwise = achieved / possible
|
|
|
|
achieved = toRational (getSum $ achievedPoints summary - achievedPassPoints summary) + scalePasses (getSum $ achievedPasses summary)
|
|
possible = toRational (getSum $ sumSheetsPoints summary - sumSheetsPassPoints summary) + scalePasses (getSum $ numSheetsPasses summary)
|
|
|
|
scalePasses :: Integer -> Rational
|
|
scalePasses passes
|
|
| pointsPossible <= 0
|
|
, Just maxPoints <- examPartMaxPoints = fromInteger passes * toRational maxPoints / fromInteger passesPossible
|
|
| pointsPossible <= 0 = 0
|
|
| passesPossible <= 0 = 0
|
|
| otherwise = fromInteger passes / (fromInteger passesPossible * passesWeights) * (toRational pointsPossible * pointsWeights)
|
|
where
|
|
passesPossible = getSum $ numSheetsPasses summary
|
|
pointsPossible = getSum $ sumSheetsPoints summary - sumSheetsPassPoints summary
|
|
|
|
pointsWeights = getSum $ foldMap (\(sWeight, gradeSummary) -> guardMonoid (sumSheetsPoints gradeSummary - sumSheetsPassPoints gradeSummary > 0) $ Sum sWeight) examSummary'
|
|
passesWeights = getSum $ foldMap (\(sWeight, gradeSummary) -> guardMonoid (numSheetsPasses gradeSummary > 0) $ Sum sWeight) examSummary'
|
|
in if | SheetGradeSummary{numMarked} <- foldOf (folded . _2) examSummary'
|
|
, numMarked <= 0
|
|
-> ExamNoShow
|
|
| otherwise
|
|
-> ExamAttended . roundToPoints . (* examPartWeight) . (* weightRescale) . getSum . fold $ foldMapM (fmap Sum . toExamPoints) examSummary'
|