feat(rating): pretty-print to new yaml based format

Parsing not implemented yet; tests should fail
This commit is contained in:
Gregor Kleen 2020-06-16 18:23:02 +02:00
parent 1195231bc3
commit 2bf484609e
14 changed files with 353 additions and 120 deletions

View File

@ -691,6 +691,7 @@ RatingMissingSeparator: Präambel der Bewertungsdatei konnte nicht identifziert
RatingMultiple: Bewertungen enthält mehrere Punktzahlen für die gleiche Abgabe RatingMultiple: Bewertungen enthält mehrere Punktzahlen für die gleiche Abgabe
RatingInvalid parseErr@Text: Bewertungspunktzahl konnte nicht als Zahl verstanden werden: #{parseErr} RatingInvalid parseErr@Text: Bewertungspunktzahl konnte nicht als Zahl verstanden werden: #{parseErr}
RatingFileIsDirectory: Bewertungsdatei ist unerlaubterweise ein Verzeichnis RatingFileIsDirectory: Bewertungsdatei ist unerlaubterweise ein Verzeichnis
RatingParseLegacyException renderedLegacyException@Text: Beim Interpretieren als Bewertungsdatei im veralteten Format: #{renderedLegacyException}
RatingNegative: Bewertungspunkte dürfen nicht negativ sein RatingNegative: Bewertungspunkte dürfen nicht negativ sein
RatingExceedsMax: Bewertung übersteigt die erlaubte Maximalpunktzahl RatingExceedsMax: Bewertung übersteigt die erlaubte Maximalpunktzahl
RatingNotExpected: Keine Bewertungen erlaubt RatingNotExpected: Keine Bewertungen erlaubt
@ -2616,4 +2617,11 @@ TestDownloadFromDatabase: Generierung während Download aus Datenbank
ValueRequiredLabeledSimple fieldLabel@Text: #{fieldLabel} wird benötigt ValueRequiredLabeledSimple fieldLabel@Text: #{fieldLabel} wird benötigt
ValueRequiredLabeledMultiWord fieldLabel@Text: „#{fieldLabel}“ wird benötigt ValueRequiredLabeledMultiWord fieldLabel@Text: „#{fieldLabel}“ wird benötigt
RatingFileTitle subId@CryptoFileNameSubmission: bewertung_#{toPathPiece subId}.txt RatingFileTitle subId@CryptoFileNameSubmission: bewertung_#{toPathPiece subId}.txt
RatingYAMLMetaComment: Meta-Informationen zur Korrektur (werden beim Hochladen ignoriert)
RatingYAMLRatingComment: Bewertung
RatingYAMLChangePointsComment: TODO: Hier die Punktezahl statt null eintragen (bis zu zwei Nachkommastellen, Punkt als Dezimalseparator; z.B. 17.03)
RatingYAMLChangePassedComment: TODO: Hier true oder false statt null eintragen (true entspricht Bestanden)
RatingYAMLChangeDoneComment: TODO: Von false auf true setzen, sobald Bewertung abgeschlossen; sonst Korrektur für die Studierenden nicht sichtbar und keine Anrechnung auf Klausurbonus
RatingYAMLChangeCommentComment: TODO: Korrektur-Kommentar für die Studierenden unterhalb der Abtrennung (...) eintragen

View File

@ -688,6 +688,7 @@ RatingMissingSeparator: Preamble of the marking file could not be identified
RatingMultiple: Correction contains multiple markings RatingMultiple: Correction contains multiple markings
RatingInvalid parseErr: Marking points could not be parsed as a number: #{parseErr} RatingInvalid parseErr: Marking points could not be parsed as a number: #{parseErr}
RatingFileIsDirectory: Marking file must not be a directory RatingFileIsDirectory: Marking file must not be a directory
RatingParseLegacyException renderedLegacyException: While parsing as a rating file in legacy format: #{renderedLegacyException}
RatingNegative: Marking points may not be negative RatingNegative: Marking points may not be negative
RatingExceedsMax: Marking points exceed maximum RatingExceedsMax: Marking points exceed maximum
RatingNotExpected: No marking points expected for this sheet RatingNotExpected: No marking points expected for this sheet
@ -2617,3 +2618,10 @@ ValueRequiredLabeledSimple fieldLabel: #{fieldLabel} is required
ValueRequiredLabeledMultiWord fieldLabel: “#{fieldLabel}” is required ValueRequiredLabeledMultiWord fieldLabel: “#{fieldLabel}” is required
RatingFileTitle subId: rating_#{toPathPiece subId}.txt RatingFileTitle subId: rating_#{toPathPiece subId}.txt
RatingYAMLMetaComment: Metadata about correction (ignored during upload)
RatingYAMLRatingComment: Rating
RatingYAMLChangePointsComment: TODO: Insert number of points instead of null (up to two decimal places, use period as a decimal separator; e.g. 17.03)
RatingYAMLChangePassedComment: TODO: Set true or false instead of null (true means passed)
RatingYAMLChangeDoneComment: TODO: Set to true instead of false, when correction is finished; otherwise correction will not be visible to students and won't be counted for exam bonus
RatingYAMLChangeCommentComment: TODO: Enter correction comment after the separator below (...)

View File

@ -147,6 +147,8 @@ dependencies:
- async - async
- pointedlist - pointedlist
- clock - clock
- HsYAML
- HsYAML-aeson
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving

View File

@ -251,6 +251,9 @@ newtype ShortWeekDay = ShortWeekDay { longWeekDay :: WeekDay }
instance RenderMessage UniWorX ShortWeekDay where instance RenderMessage UniWorX ShortWeekDay where
renderMessage _ ls (ShortWeekDay wDay) = pack . snd $ wDays (getTimeLocale' ls) !! (fromEnum wDay `mod` 7) renderMessage _ ls (ShortWeekDay wDay) = pack . snd $ wDays (getTimeLocale' ls) !! (fromEnum wDay `mod` 7)
instance Default DateTimeFormatter where
def = mkDateTimeFormatter (getTimeLocale' []) def appTZ
-- Access Control -- Access Control
newtype InvalidAuthTag = InvalidAuthTag Text newtype InvalidAuthTag = InvalidAuthTag Text

View File

@ -182,7 +182,6 @@ embedRenderMessage ''UniWorX ''StudyFieldType id
embedRenderMessage ''UniWorX ''SheetFileType id embedRenderMessage ''UniWorX ''SheetFileType id
embedRenderMessage ''UniWorX ''SubmissionFileType id embedRenderMessage ''UniWorX ''SubmissionFileType id
embedRenderMessage ''UniWorX ''CorrectorState id embedRenderMessage ''UniWorX ''CorrectorState id
embedRenderMessage ''UniWorX ''RatingException id
embedRenderMessage ''UniWorX ''SubmissionSinkException ("SubmissionSinkException" <>) embedRenderMessage ''UniWorX ''SubmissionSinkException ("SubmissionSinkException" <>)
embedRenderMessage ''UniWorX ''SheetGrading ("SheetGrading" <>) embedRenderMessage ''UniWorX ''SheetGrading ("SheetGrading" <>)
embedRenderMessage ''UniWorX ''AuthTag $ ("AuthTag" <>) . concat . drop 1 . splitCamel embedRenderMessage ''UniWorX ''AuthTag $ ("AuthTag" <>) . concat . drop 1 . splitCamel
@ -213,6 +212,18 @@ instance RenderMessage UniWorX RatingFileException where
mr :: RenderMessage UniWorX msg => msg -> Text mr :: RenderMessage UniWorX msg => msg -> Text
mr = renderMessage foundation ls mr = renderMessage foundation ls
embedRenderMessage ''UniWorX ''RatingParseLegacyException id
instance RenderMessage UniWorX RatingException where
renderMessage foundation ls = \case
RatingParseLegacyException legacyException -> mr . MsgRatingParseLegacyException $ mr legacyException
RatingNegative -> mr MsgRatingNegative
RatingExceedsMax -> mr MsgRatingExceedsMax
RatingNotExpected -> mr MsgRatingNotExpected
RatingBinaryExpected -> mr MsgRatingBinaryExpected
RatingPointsRequired -> mr MsgRatingPointsRequired
where mr :: RenderMessage UniWorX msg => msg -> Text
mr = renderMessage foundation ls
newtype ShortSex = ShortSex Sex newtype ShortSex = ShortSex Sex
embedRenderMessageVariant ''UniWorX ''ShortSex ("Short" <>) embedRenderMessageVariant ''UniWorX ''ShortSex ("Short" <>)

View File

@ -76,6 +76,7 @@ postCorrectionR tid ssh csh shn cid = do
{ ratingPoints = ratingPoints' { ratingPoints = ratingPoints'
, ratingComment = ratingComment' , ratingComment = ratingComment'
, ratingTime = guardOn rated now , ratingTime = guardOn rated now
, ratingDone = rated
} }
guardValidation MsgSubmissionCannotBeRatedWithoutCorrector $ isn't _Nothing ratingBy' || not rated guardValidation MsgSubmissionCannotBeRatedWithoutCorrector $ isn't _Nothing ratingBy' || not rated

View File

@ -10,6 +10,7 @@ module Handler.Utils.DateTime
, formatTime, formatTimeW, formatTimeMail , formatTime, formatTimeW, formatTimeMail
, formatTimeRange, formatTimeRangeW, formatTimeRangeMail , formatTimeRange, formatTimeRangeW, formatTimeRangeMail
, getTimeLocale, getDateTimeFormat , getTimeLocale, getDateTimeFormat
, getDateTimeFormatter
, validDateTimeFormats, dateTimeFormatOptions , validDateTimeFormats, dateTimeFormatOptions
, addOneWeek, addWeeks , addOneWeek, addWeeks
, weeksToAdd , weeksToAdd
@ -23,15 +24,11 @@ import Import
import Data.Time.Zones import Data.Time.Zones
import qualified Data.Time.Zones as TZ import qualified Data.Time.Zones as TZ
import Data.Time hiding (formatTime, localTimeToUTC, utcToLocalTime, utcToZonedTime)
-- import Data.Time.Clock (addUTCTime,nominalDay)
import qualified Data.Time.Format as Time import qualified Data.Time.Format as Time
import Data.Time.Format.ISO8601 (iso8601Show) import Data.Time.Format.ISO8601 (iso8601Show)
import qualified Data.Set as Set import qualified Data.Set as Set
import Data.Time.Clock.System (systemEpochDay)
import qualified Data.Csv as Csv import qualified Data.Csv as Csv
import qualified Data.Char as Char import qualified Data.Char as Char
@ -69,22 +66,9 @@ toMorning = toTimeOfDay 6 0 0
toTimeOfDay :: Int -> Int -> Pico -> Day -> UTCTime toTimeOfDay :: Int -> Int -> Pico -> Day -> UTCTime
toTimeOfDay todHour todMin todSec d = localTimeToUTCTZ appTZ $ LocalTime d TimeOfDay{..} toTimeOfDay todHour todMin todSec d = localTimeToUTCTZ appTZ $ LocalTime d TimeOfDay{..}
class FormatTime t => HasLocalTime t where
toLocalTime :: t -> LocalTime
instance HasLocalTime LocalTime where
toLocalTime = id
instance HasLocalTime Day where
toLocalTime d = LocalTime d midnight
instance HasLocalTime UTCTime where instance HasLocalTime UTCTime where
toLocalTime = utcToLocalTime toLocalTime = utcToLocalTime
instance HasLocalTime TimeOfDay where
toLocalTime = LocalTime systemEpochDay
formatTime' :: (HasLocalTime t, MonadHandler m) => String -> t -> m Text formatTime' :: (HasLocalTime t, MonadHandler m) => String -> t -> m Text
formatTime' fmtStr t = fmap fromString $ Time.formatTime <$> getTimeLocale <*> pure fmtStr <*> pure (utcToZonedTime . localTimeToUTCTZ appTZ $ toLocalTime t) formatTime' fmtStr t = fmap fromString $ Time.formatTime <$> getTimeLocale <*> pure fmtStr <*> pure (utcToZonedTime . localTimeToUTCTZ appTZ $ toLocalTime t)
@ -123,6 +107,12 @@ getDateTimeFormat sel = do
SelFormatTime -> userDefaultTimeFormat SelFormatTime -> userDefaultTimeFormat
return fmt return fmt
getDateTimeFormatter :: (MonadHandler m, HandlerSite m ~ UniWorX) => m DateTimeFormatter
getDateTimeFormatter = do
locale <- getTimeLocale
formatMap <- traverse getDateTimeFormat id
return $ mkDateTimeFormatter locale formatMap appTZ
validDateTimeFormats :: TimeLocale -> SelDateTimeFormat -> Set DateTimeFormat validDateTimeFormats :: TimeLocale -> SelDateTimeFormat -> Set DateTimeFormat
-- ^ We use a whitelist instead of just letting the user specify their own format string since vulnerabilities in printf-like functions are not uncommon -- ^ We use a whitelist instead of just letting the user specify their own format string since vulnerabilities in printf-like functions are not uncommon
validDateTimeFormats tl SelFormatDateTime = Set.fromList $ validDateTimeFormats tl SelFormatDateTime = Set.fromList $

View File

@ -3,8 +3,6 @@ module Handler.Utils.Rating
, validateRating , validateRating
, getRating , getRating
, ratingFile , ratingFile
, RatingException(..)
, UnicodeException(..)
, isRatingFile , isRatingFile
, SubmissionContent , SubmissionContent
, extractRatings , extractRatings
@ -13,6 +11,8 @@ module Handler.Utils.Rating
import Import import Import
import Handler.Utils.DateTime (getDateTimeFormatter)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.ByteString.Lazy as Lazy.ByteString import qualified Data.ByteString.Lazy as Lazy.ByteString
@ -42,14 +42,15 @@ validateRating ratingSheetType Rating'{ ratingPoints=Just rp, .. }
validateRating ratingSheetType Rating'{ .. } validateRating ratingSheetType Rating'{ .. }
| has _grading ratingSheetType | has _grading ratingSheetType
, is _Nothing ratingPoints , is _Nothing ratingPoints
, isn't _Nothing ratingTime , ratingDone
, hasn't (_grading . _PassAlways) ratingSheetType , hasn't (_grading . _PassAlways) ratingSheetType
= [RatingPointsRequired] = [RatingPointsRequired]
validateRating _ _ = [] validateRating _ _ = []
getRating :: SubmissionId -> YesodDB UniWorX (Maybe Rating) getRating :: SubmissionId -> YesodDB UniWorX (Maybe Rating)
getRating submissionId = runMaybeT $ do getRating submissionId = runMaybeT $ do
let query = E.select . E.from $ \(corrector `E.RightOuterJoin` (submission `E.InnerJoin` sheet `E.InnerJoin` course)) -> do let query = E.select . E.from $ \(corrector `E.RightOuterJoin` (submission `E.InnerJoin` sheet `E.InnerJoin` course `E.InnerJoin` school)) -> do
E.on $ course E.^. CourseSchool E.==. school E.^. SchoolId
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
E.on $ corrector E.?. UserId E.==. submission E.^. SubmissionRatingBy E.on $ corrector E.?. UserId E.==. submission E.^. SubmissionRatingBy
@ -57,24 +58,29 @@ getRating submissionId = runMaybeT $ do
E.where_ $ submission E.^. SubmissionId E.==. E.val submissionId E.where_ $ submission E.^. SubmissionId E.==. E.val submissionId
-- Yes, we can only pass a tuple through 'E.select' -- Yes, we can only pass a tuple through 'E.select'
return ( course E.^. CourseName return ( course E.^. CourseTerm
, school E.^. SchoolName
, course E.^. CourseName
, sheet E.^. SheetName , sheet E.^. SheetName
, corrector E.?. UserDisplayName , corrector E.?. UserDisplayName
, sheet E.^. SheetType , sheet E.^. SheetType
, submission E.^. SubmissionRatingPoints , submission
, submission E.^. SubmissionRatingComment
, submission E.^. SubmissionRatingTime
) )
[ ( E.unValue -> ratingCourseName [ ( unTermKey . E.unValue -> ratingCourseTerm
, E.unValue -> ratingCourseSchool
, E.unValue -> ratingCourseName
, E.unValue -> ratingSheetName , E.unValue -> ratingSheetName
, E.unValue -> ratingCorrectorName , E.unValue -> ratingCorrectorName
, E.unValue -> ratingSheetType , E.unValue -> ratingSheetType
, E.unValue -> ratingPoints , E.Entity _ sub@Submission{..}
, E.unValue -> ratingComment
, E.unValue -> ratingTime
) ] <- lift query ) ] <- lift query
let ratingPoints = submissionRatingPoints
ratingComment = submissionRatingComment
ratingTime = submissionRatingTime
ratingDone = submissionRatingDone sub
return Rating{ ratingValues = Rating'{..}, .. } return Rating{ ratingValues = Rating'{..}, .. }
extensionRating :: String extensionRating :: String
@ -85,11 +91,12 @@ ratingFile :: ( MonadHandler m
) )
=> CryptoFileNameSubmission -> Rating -> m File => CryptoFileNameSubmission -> Rating -> m File
ratingFile cID rating@Rating{ ratingValues = Rating'{..}, .. } = do ratingFile cID rating@Rating{ ratingValues = Rating'{..}, .. } = do
MsgRenderer mr <- getMsgRenderer mr'@(MsgRenderer mr) <- getMsgRenderer
dtFmt <- getDateTimeFormatter
fileModified <- maybe (liftIO getCurrentTime) return ratingTime fileModified <- maybe (liftIO getCurrentTime) return ratingTime
let let
fileTitle = ensureExtension extensionRating . unpack . mr $ MsgRatingFileTitle cID fileTitle = ensureExtension extensionRating . unpack . mr $ MsgRatingFileTitle cID
fileContent = Just . Lazy.ByteString.toStrict $ formatRating cID rating fileContent = Just . Lazy.ByteString.toStrict $ formatRating mr' dtFmt cID rating
return File{..} return File{..}
where ensureExtension ext fName = bool (`addExtension` ext) id (ext `isExtensionOf` fName) fName where ensureExtension ext fName = bool (`addExtension` ext) id (ext `isExtensionOf` fName) fName

View File

@ -1,4 +1,4 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS -Wno-error=deprecations #-}
module Handler.Utils.Rating.Format module Handler.Utils.Rating.Format
( parseRating, formatRating ( parseRating, formatRating
@ -6,81 +6,162 @@ module Handler.Utils.Rating.Format
import Import import Import
import Text.PrettyPrint.Leijen.Text hiding ((<$>)) import Handler.Utils.DateTime ()
import Text.Read (readEither)
import qualified Data.Map.Strict as Map
import qualified Data.Char as Char
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text import qualified Data.Text.Lazy as Lazy.Text
import qualified Data.Text.Lazy.Encoding as Lazy.Text import qualified Data.Text.Lazy.Encoding as Lazy.Text
import qualified Data.ByteString.Lazy as Lazy (ByteString) import qualified Data.ByteString.Lazy as Lazy (ByteString)
import qualified Data.ByteString.Lazy as Lazy.ByteString
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
instance HasResolution prec => Pretty (Fixed prec) where import qualified Handler.Utils.Rating.Format.Legacy as Legacy
pretty = pretty . show
instance Pretty x => Pretty (CI x) where import qualified Data.YAML as YAML
pretty = pretty . CI.original import qualified Data.YAML.Event as YAML (untagged)
import qualified Data.YAML.Event as YAML.Event
import qualified Data.YAML.Token as YAML (Encoding(..))
import Data.YAML.Aeson () -- ToYAML Value
import Data.List (elemIndex)
import Control.Monad.Trans.State.Lazy (evalState)
instance Pretty SheetGrading where data PrettifyState
pretty Points{..} = pretty ( "Maximal " <> show maxPoints <> " Punkt(e)" :: String) = PrettifyInitial
pretty PassPoints{..} = pretty ( "Maximal " <> show maxPoints <> " Punkt(e), bestanden ab " <> show passingPoints <> " Punkt(en)" :: String ) | PrettifyMetadata Natural
pretty PassBinary = pretty ( "Bestanden (1) / Nicht bestanden (0)" :: String ) | PrettifyRatingPoints
pretty PassAlways = pretty ( "Automatisch bestanden, sobald korrigiert" :: String ) | PrettifyRatingPassed
| PrettifyRating
| PrettifyRatingDone
| PrettifyComment
deriving (Eq, Ord, Read, Show, Generic, Typeable)
formatRating :: CryptoFileNameSubmission -> Rating -> Lazy.ByteString formatRating :: MsgRendererS UniWorX -> DateTimeFormatter -> CryptoFileNameSubmission -> Rating -> Lazy.ByteString
formatRating cID Rating{ ratingValues = Rating'{..}, ..} = let formatRating (MsgRenderer mr) DateTimeFormatter{..} cID Rating{ ratingValues = Rating'{..}, .. } = mconcat
doc = renderPretty 1 45 . foldr (<$$>) mempty $ catMaybes [ prettyYAML
[ pure "= Bitte nur Bewertung und Kommentare ändern =" , maybe Lazy.ByteString.empty (Lazy.Text.encodeUtf8 . Lazy.Text.fromStrict) ratingComment
, pure "=============================================" ]
, pure "========== Uni2work Bewertungsdatei =========" where
, pure "======= diese Datei ist UTF8 encodiert ======" uglyYAML = YAML.Event.writeEvents YAML.UTF8 $ concat
, pure "Informationen zum Übungsblatt:" [ [ YAML.Event.StreamStart
, pure . indent 2 . foldr (<$$>) mempty . catMaybes $ , YAML.Event.DocumentStart $ YAML.Event.DirEndMarkerVersion 2
[ Just $ "Veranstaltung:" <+> pretty ratingCourseName ]
, Just $ "Blatt:" <+> pretty ratingSheetName , concat
, ("Korrektor:" <+>) . pretty <$> ratingCorrectorName [ pure $ YAML.Event.MappingStart Nothing YAML.untagged YAML.Event.Block
, ("Bewertungsschema:" <+>) . pretty <$> (ratingSheetType ^? _grading) , mapEvents (str' "term") (msg $ ShortTermIdentifier ratingCourseTerm)
, mapEvents (str' "school") (str $ CI.original ratingCourseSchool)
, mapEvents (str' "course") (str $ CI.original ratingCourseName)
, nodeEvents (str' "sheet")
, case YAML.toYAML $ toJSON ratingSheetType of
YAML.Mapping _ _ typeMap
-> let typeMap' = flip sortOn (Map.toList typeMap) $ \case
(YAML.Scalar _ (YAML.SStr k), _) -> NTop $ k `elemIndex` ["type", "grading"]
_other -> NTop Nothing
in concat
[ pure $ YAML.Event.MappingStart Nothing YAML.untagged YAML.Event.Block
, mapEvents (str' "name") (str $ CI.original ratingSheetName)
, concat [ mapEvents k v | (k, v) <- typeMap' ]
, pure YAML.Event.MappingEnd
]
_other -> nodeEvents . str $ CI.original ratingSheetName
, mapEvents (str' "submission") (str $ toPathPiece cID)
, mapEvents (str' "rated_by") (maybe (YAML.Scalar () YAML.SNull) str ratingCorrectorName)
, mapEvents (str' "rated_at") (maybe (YAML.Scalar () YAML.SNull) (str . format SelFormatDateTime) ratingTime)
, case ratingSheetType ^? _grading of
Nothing -> mempty
Just mode -> if
| is _PassAlways mode
-> mempty
| has _passingBound mode
-> mapEvents (str' "passed") (maybe (YAML.Scalar () YAML.SNull) (YAML.Scalar () . YAML.SBool) $ gradingPassed mode =<< ratingPoints)
| otherwise
-> mapEvents (str' "points") (maybe (YAML.Scalar () YAML.SNull) (YAML.Scalar () . YAML.SFloat . realToFrac) ratingPoints)
, mapEvents (str' "rating_done") (YAML.Scalar () $ YAML.SBool ratingDone)
, pure $ YAML.Event.MappingEnd
]
, [ YAML.Event.DocumentEnd True
, YAML.Event.StreamEnd
]
] ]
, pure $ "Abgabe-Id:" <+> pretty (Text.unpack $ toPathPiece cID) where str :: forall t. Textual t => t -> YAML.Node ()
, guardOn (hasn't (_grading . _PassAlways) ratingSheetType) "=============================================" str = YAML.Scalar () . YAML.SStr . repack
, guardOn (hasn't (_grading . _PassAlways) ratingSheetType) $ "Bewertung:" <+> pretty ratingPoints str' :: Text -> YAML.Node ()
, pure "=========== Beginn der Kommentare ===========" str' = str
, pure $ pretty ratingComment msg :: forall msg. RenderMessage UniWorX msg => msg -> YAML.Node ()
] msg = str . mr
in Lazy.Text.encodeUtf8 . (<> "\n") $ displayT doc
parseRating :: MonadThrow m => File -> m Rating' mapEvents :: YAML.Node () -> YAML.Node () -> [YAML.Event.Event]
parseRating File{ fileContent = Just input, .. } = do mapEvents k v = filterEvs . nodeEvents . YAML.Mapping () YAML.untagged $ singletonMap k v
inputText <- either (throwM . RatingNotUnicode) return $ Text.decodeUtf8' input where filterEvs ((YAML.Event.MappingStart _ _ _ : inner) :> YAML.Event.MappingEnd) = inner
let filterEvs _other = error "Could not strip Mapping"
(headerLines', commentLines) = break (commentSep `Text.isInfixOf`) $ Text.lines inputText
(reverse -> ratingLines, reverse -> _headerLines) = break (sep' `Text.isInfixOf`) $ reverse headerLines' nodeEvents :: YAML.Node () -> [YAML.Event.Event]
ratingLines' = filter (rating `Text.isInfixOf`) ratingLines nodeEvents = filterEvs . mapMaybe (fmap YAML.Event.eEvent . preview _Right) . YAML.Event.parseEvents . YAML.encodeNode . pure . YAML.Doc
commentSep :: Text where filterEvs = filter $ \case
commentSep = "Beginn der Kommentare" YAML.Event.StreamStart -> False
sep' = Text.pack $ replicate 40 '=' YAML.Event.StreamEnd -> False
rating :: Text YAML.Event.DocumentStart _ -> False
rating = "Bewertung:" YAML.Event.DocumentEnd _ -> False
comment' <- case commentLines of _other -> True
(_:commentLines') -> return . Text.strip $ Text.unlines commentLines'
_ -> throwM RatingMissingSeparator prettyYAML = annotate . (evalState ?? PrettifyInitial) . transduce' $ YAML.Event.parseEvents uglyYAML
let where
ratingComment transduce' (Left _ : _) = error "Parse error on uglyYAML"
| Text.null comment' = Nothing transduce' (Right YAML.Event.EvPos{ eEvent, ePos = pos1 } : es@(Right YAML.Event.EvPos{ ePos = pos2 }: _))
| otherwise = Just comment' = (:) <$> ((YAML.Event.posByteOffset pos1, YAML.Event.posByteOffset pos2, ) <$> state (flip transduce eEvent)) <*> transduce' es
ratingLine' <- case ratingLines' of transduce' (Right YAML.Event.EvPos{..} : es@_)
[] -> return Text.empty = (:) <$> ((YAML.Event.posByteOffset ePos, fromIntegral $ Lazy.ByteString.length uglyYAML, ) <$> state (flip transduce eEvent)) <*> transduce' es
[l] -> return l transduce' [] = return []
_ -> throwM RatingMultiple
let annotate = fst . foldl' annotate' (uglyYAML, Lazy.ByteString.length uglyYAML) . reverse
(_, ratingLine) = Text.breakOnEnd rating ratingLine' where annotate' (dat, mLength) (fromIntegral -> pos1, fromIntegral -> pos2, (fromStrict . encodeUtf8 -> ann1, ann2))
ratingStr = Text.unpack $ Text.strip ratingLine = let (before', after) = Lazy.ByteString.splitAt pos2' dat
ratingPoints <- case () of (before, event) = Lazy.ByteString.splitAt pos1' before'
_ | null ratingStr -> return Nothing event' = decodeUtf8 $ toStrict event
| otherwise -> either (throwM . RatingInvalid . pack) return $ Just <$> readEither ratingStr ws = Text.takeWhileEnd Char.isSpace event'
return Rating'{ ratingTime = Just fileModified, .. } event'' = Text.dropWhileEnd Char.isSpace event'
parseRating _ = throwM RatingFileIsDirectory pos1' = min pos1 mLength
pos2' = min pos2 mLength
in (before <> ann1 <> fromStrict (encodeUtf8 event'') <> fromStrict (encodeUtf8 $ ann2 ws) <> after, pos1')
transduce :: PrettifyState -> YAML.Event.Event -> ((Text, Text -> Text), PrettifyState)
transduce PrettifyInitial (YAML.Event.MappingStart _ _ _) = (("# " <> mr MsgRatingYAMLMetaComment <> "\n", id), PrettifyMetadata 0)
transduce PrettifyInitial _ = ((mempty, id), PrettifyInitial)
transduce (PrettifyMetadata 0) (YAML.Event.Scalar _ _ _ k)
| k == "points" = ((startRatingComment, id), PrettifyRatingPoints)
| k == "passed" = ((startRatingComment, id), PrettifyRatingPassed)
| k == "rating_done" = ((startRatingComment, id), PrettifyRatingDone)
where startRatingComment = "\n# " <> mr MsgRatingYAMLRatingComment <> "\n"
transduce (PrettifyMetadata n) (YAML.Event.MappingStart _ _ _) = ((mempty, id), PrettifyMetadata $ succ n)
transduce (PrettifyMetadata 0) _ = ((mempty, id), PrettifyMetadata 0)
transduce (PrettifyMetadata n) YAML.Event.MappingEnd = ((mempty, id), PrettifyMetadata $ pred n)
transduce cState@(PrettifyMetadata _) _ = ((mempty, id), cState)
transduce PrettifyRatingPoints _ = ((mempty, beforeBreak $ " # " <> mr MsgRatingYAMLChangePointsComment), PrettifyRating)
transduce PrettifyRatingPassed _ = ((mempty, beforeBreak $ " # " <> mr MsgRatingYAMLChangePassedComment), PrettifyRating)
transduce PrettifyRatingDone _ = ((mempty, beforeBreak $ " # " <> mr MsgRatingYAMLChangeDoneComment), PrettifyRating)
transduce PrettifyRating (YAML.Event.Scalar _ _ _ k)
| k == "points" = ((mempty, id), PrettifyRatingPoints)
| k == "passed" = ((mempty, id), PrettifyRatingPassed)
| k == "rating_done" = ((mempty, id), PrettifyRatingDone)
transduce PrettifyRating YAML.Event.MappingEnd = (("\n", (<> ("# " <> mr MsgRatingYAMLChangeCommentComment <> "\n"))), PrettifyComment)
transduce PrettifyRating _ = ((mempty, id), PrettifyRating)
transduce PrettifyComment _ = ((mempty, id), PrettifyComment)
-- transduce cState _ = (("<", \ws -> "|" <> ws <> ">"), cState) -- TODO
beforeBreak :: Text -> Text -> Text
beforeBreak ins ws = before <> ins <> break' <> after
where (before', after) = Text.breakOnEnd "\n" ws
before = Text.dropWhileEnd (== '\n') before'
break' = Text.takeWhileEnd (== '\n') before'
parseRating :: MonadCatch m => File -> m Rating'
parseRating = Legacy.parseRating

View File

@ -0,0 +1,88 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Utils.Rating.Format.Legacy
( parseRating, formatRating
) where
import Import
import Text.PrettyPrint.Leijen.Text hiding ((<$>))
import qualified Data.Text as Text
import qualified Data.Text.Lazy.Encoding as Lazy.Text
import qualified Data.Text.Encoding as Text
import qualified Data.ByteString.Lazy as Lazy (ByteString)
import qualified Data.CaseInsensitive as CI
import Text.Read (readEither)
instance HasResolution prec => Pretty (Fixed prec) where
pretty = pretty . show
instance Pretty x => Pretty (CI x) where
pretty = pretty . CI.original
instance Pretty SheetGrading where
pretty Points{..} = pretty ( "Maximal " <> show maxPoints <> " Punkt(e)" :: String)
pretty PassPoints{..} = pretty ( "Maximal " <> show maxPoints <> " Punkt(e), bestanden ab " <> show passingPoints <> " Punkt(en)" :: String )
pretty PassBinary = pretty ( "Bestanden (1) / Nicht bestanden (0)" :: String )
pretty PassAlways = pretty ( "Automatisch bestanden, sobald korrigiert" :: String )
formatRating :: CryptoFileNameSubmission -> Rating -> Lazy.ByteString
formatRating cID Rating{ ratingValues = Rating'{..}, ..} = let
doc = renderPretty 1 45 . foldr (<$$>) mempty $ catMaybes
[ pure "= Bitte nur Bewertung und Kommentare ändern ="
, pure "============================================="
, pure "========== Uni2work Bewertungsdatei ========="
, pure "======= diese Datei ist UTF8 encodiert ======"
, pure "Informationen zum Übungsblatt:"
, pure . indent 2 . foldr (<$$>) mempty . catMaybes $
[ Just $ "Veranstaltung:" <+> pretty ratingCourseName
, Just $ "Blatt:" <+> pretty ratingSheetName
, ("Korrektor:" <+>) . pretty <$> ratingCorrectorName
, ("Bewertungsschema:" <+>) . pretty <$> (ratingSheetType ^? _grading)
]
, pure $ "Abgabe-Id:" <+> pretty (Text.unpack $ toPathPiece cID)
, guardOn (hasn't (_grading . _PassAlways) ratingSheetType) "============================================="
, guardOn (hasn't (_grading . _PassAlways) ratingSheetType) $ "Bewertung:" <+> pretty ratingPoints
, pure "=========== Beginn der Kommentare ==========="
, pure $ pretty ratingComment
]
in Lazy.Text.encodeUtf8 . (<> "\n") $ displayT doc
parseRating :: MonadCatch m => File -> m Rating'
parseRating File{ fileContent = Just input, .. } = handle (throwM . RatingParseLegacyException) $ do
inputText <- either (throwM . RatingNotUnicode) return $ Text.decodeUtf8' input
let
(headerLines', commentLines) = break (commentSep `Text.isInfixOf`) $ Text.lines inputText
(reverse -> ratingLines, reverse -> _headerLines) = break (sep' `Text.isInfixOf`) $ reverse headerLines'
ratingLines' = filter (rating `Text.isInfixOf`) ratingLines
commentSep :: Text
commentSep = "Beginn der Kommentare"
sep' = Text.pack $ replicate 40 '='
rating :: Text
rating = "Bewertung:"
comment' <- case commentLines of
(_:commentLines') -> return . Text.strip $ Text.unlines commentLines'
_ -> throwM RatingMissingSeparator
let
ratingComment
| Text.null comment' = Nothing
| otherwise = Just comment'
ratingLine' <- case ratingLines' of
[] -> return Text.empty
[l] -> return l
_ -> throwM RatingMultiple
let
(_, ratingLine) = Text.breakOnEnd rating ratingLine'
ratingStr = Text.unpack $ Text.strip ratingLine
ratingPoints <- case () of
_ | null ratingStr -> return Nothing
| otherwise -> either (throwM . RatingInvalid . pack) return $ Just <$> readEither ratingStr
return Rating'{ ratingDone = False, ratingTime = Just fileModified, .. }
parseRating _ = throwM RatingFileIsDirectory

View File

@ -563,8 +563,8 @@ sinkSubmission userId mExists isUpdate = do
submission <- lift $ getJust submissionId submission <- lift $ getJust submissionId
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let rated = ratingDone r
let let
rated = fromMaybe False $ (==) <$> submissionRatingBy submission <*> userId -- FIXME: This behaviour is unintuitive and needs to be replaced with an "isDone"-Field in rating files
r'@Rating'{..} = r r'@Rating'{..} = r
{ ratingTime = now <$ guard rated -- Ignore `ratingTime` from result @r@ of `parseRating` to ensure plausible timestamps (`parseRating` returns file modification time for consistency with `ratingFile`) { ratingTime = now <$ guard rated -- Ignore `ratingTime` from result @r@ of `parseRating` to ensure plausible timestamps (`parseRating` returns file modification time for consistency with `ratingFile`)
} }
@ -572,7 +572,7 @@ sinkSubmission userId mExists isUpdate = do
{ submissionRatingPoints = ratingPoints { submissionRatingPoints = ratingPoints
, submissionRatingComment = ratingComment , submissionRatingComment = ratingComment
, submissionRatingTime = ratingTime , submissionRatingTime = ratingTime
, submissionRatingBy = userId <* guard rated -- This is never an update due to the definition of rated; this is done so idempotency of uploads is maintained (FIXME: when "isDone"-Field is introduced, set this to `Just userId`) , submissionRatingBy = userId
} }
tellSt $ mempty{ sinkSeenRating = Last $ Just r' } tellSt $ mempty{ sinkSeenRating = Last $ Just r' }

View File

@ -8,7 +8,9 @@ import Data.Text.Encoding.Error (UnicodeException(..))
data Rating = Rating data Rating = Rating
{ ratingCourseName :: CourseName { ratingCourseTerm :: TermIdentifier
, ratingCourseSchool :: SchoolName
, ratingCourseName :: CourseName
, ratingSheetName :: SheetName , ratingSheetName :: SheetName
, ratingCorrectorName :: Maybe Text , ratingCorrectorName :: Maybe Text
, ratingSheetType :: SheetType , ratingSheetType :: SheetType
@ -19,18 +21,25 @@ data Rating' = Rating'
{ ratingPoints :: Maybe Points { ratingPoints :: Maybe Points
, ratingComment :: Maybe Text , ratingComment :: Maybe Text
, ratingTime :: Maybe UTCTime , ratingTime :: Maybe UTCTime
, ratingDone :: Bool
} deriving (Read, Show, Eq, Generic, Typeable) } deriving (Read, Show, Eq, Generic, Typeable)
data RatingException = RatingNotUnicode UnicodeException -- ^ Rating failed to parse as unicode data RatingParseLegacyException
| RatingMissingSeparator -- ^ Could not split rating header from comments = RatingNotUnicode UnicodeException -- ^ Rating failed to parse as unicode
| RatingMultiple -- ^ Encountered multiple point values in rating | RatingMissingSeparator -- ^ Could not split rating header from comments
| RatingInvalid Text -- ^ Failed to parse rating point value | RatingMultiple -- ^ Encountered multiple point values in rating
| RatingFileIsDirectory -- ^ We do not expect this to, it's included for totality | RatingInvalid Text -- ^ Failed to parse rating point value
| RatingNegative -- ^ Rating points must be non-negative | RatingFileIsDirectory -- ^ We do not expect this to, it's included for totality
| RatingExceedsMax -- ^ Rating point must not exceed maximum points deriving (Show, Eq, Generic, Typeable)
| RatingNotExpected -- ^ Rating not expected deriving anyclass (Exception)
| RatingBinaryExpected -- ^ Rating must be 0 or 1
| RatingPointsRequired -- ^ Rating without points for sheet that requires there to be points data RatingException
= RatingParseLegacyException RatingParseLegacyException
| RatingNegative -- ^ Rating points must be non-negative
| RatingExceedsMax -- ^ Rating point must not exceed maximum points
| RatingNotExpected -- ^ Rating not expected
| RatingBinaryExpected -- ^ Rating must be 0 or 1
| RatingPointsRequired -- ^ Rating without points for sheet that requires there to be points
deriving (Show, Eq, Generic, Typeable) deriving (Show, Eq, Generic, Typeable)
deriving anyclass (Exception) deriving anyclass (Exception)

View File

@ -4,20 +4,26 @@ module Utils.DateTime
( timeLocaleMap ( timeLocaleMap
, TimeLocale(..) , TimeLocale(..)
, currentYear , currentYear
, HasLocalTime(..)
, DateTimeFormat(..) , DateTimeFormat(..)
, SelDateTimeFormat(..) , SelDateTimeFormat(..)
, DateTimeFormatter(..)
, mkDateTimeFormatter
, nominalHour, nominalMinute , nominalHour, nominalMinute
, minNominalYear, avgNominalYear , minNominalYear, avgNominalYear
, module Data.Time.Zones , module Zones
, module Data.Time.Zones.TH
) where ) where
import ClassyPrelude.Yesod hiding (lift) import ClassyPrelude.Yesod hiding (lift)
import System.Locale.Read import System.Locale.Read
import Data.Time (NominalDiffTime, nominalDay) import Data.Time (NominalDiffTime, nominalDay, LocalTime(..), TimeOfDay, midnight, ZonedTime(..))
import Data.Time.Zones (TZ) import Data.Time.Zones as Zones (TZ)
import Data.Time.Zones.TH (includeSystemTZ) import Data.Time.Zones.TH as Zones (includeSystemTZ)
import Data.Time.Zones (localTimeToUTCTZ, timeZoneForUTCTime)
import Data.Time.Format (FormatTime)
import Data.Time.Clock.System (systemEpochDay)
import qualified Data.Time.Format as Time
import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List.NonEmpty as NonEmpty
@ -72,6 +78,17 @@ currentYear = do
let (year, _, _) = toGregorian $ utctDay now let (year, _, _) = toGregorian $ utctDay now
[e|year|] [e|year|]
class FormatTime t => HasLocalTime t where
toLocalTime :: t -> LocalTime
instance HasLocalTime LocalTime where
toLocalTime = id
instance HasLocalTime Day where
toLocalTime d = LocalTime d midnight
instance HasLocalTime TimeOfDay where
toLocalTime = LocalTime systemEpochDay
newtype DateTimeFormat = DateTimeFormat { unDateTimeFormat :: String } newtype DateTimeFormat = DateTimeFormat { unDateTimeFormat :: String }
deriving (Eq, Ord, Read, Show, Data, Generic, Typeable) deriving (Eq, Ord, Read, Show, Data, Generic, Typeable)
@ -105,6 +122,10 @@ instance BoundedJoinSemiLattice SelDateTimeFormat where
instance BoundedMeetSemiLattice SelDateTimeFormat where instance BoundedMeetSemiLattice SelDateTimeFormat where
top = SelFormatDateTime top = SelFormatDateTime
data DateTimeFormatter = DateTimeFormatter { format :: forall t. HasLocalTime t => SelDateTimeFormat -> t -> Text }
mkDateTimeFormatter :: TimeLocale -> (SelDateTimeFormat -> DateTimeFormat) -> TZ -> DateTimeFormatter
mkDateTimeFormatter locale formatMap appTZ = DateTimeFormatter (\(formatMap -> fmt) t -> pack . Time.formatTime locale (unDateTimeFormat fmt) $ ZonedTime (toLocalTime t) (timeZoneForUTCTime appTZ . localTimeToUTCTZ appTZ $ toLocalTime t))
--------------------- ---------------------
-- NominalDiffTime -- -- NominalDiffTime --

View File

@ -10,18 +10,22 @@ import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as Text import qualified Data.Text as Text
import Utils (assertM) import Utils (assertM, MsgRendererS(..))
import Text.Shakespeare.I18N (renderMessage)
spec :: Spec spec :: Spec
spec = describe "Rating file parsing/pretty-printing" $ do spec = describe "Rating file parsing/pretty-printing" $ do
it "roundtrips" . property $ \(_ :: SubmissionId, subId) (mRating -> rating) -> it "roundtrips" . property $ \(_ :: SubmissionId, subId) (mRating -> rating) ->
parseRating' (formatRating subId rating) === Just (ratingValues rating) parseRating' (formatRating mr' def subId rating) === Just (ratingValues rating)
it "has idempotent formatting" . property $ \(_ :: SubmissionId, subId) (mRating -> rating) -> it "has idempotent formatting" . property $ \(_ :: SubmissionId, subId) (mRating -> rating) ->
fmap (\r' -> formatRating subId $ rating { ratingValues = r' }) (parseRating' $ formatRating subId rating) === Just (formatRating subId rating) fmap (\r' -> formatRating mr' def subId $ rating { ratingValues = r' }) (parseRating' $ formatRating mr' def subId rating) === Just (formatRating mr' def subId rating)
where where
mr' :: forall site. MsgRendererS site
mr' = MsgRenderer $ renderMessage (error "foundation inspected" :: site) []
parseRating' :: LBS.ByteString -> Maybe Rating' parseRating' :: LBS.ByteString -> Maybe Rating'
parseRating' = parseRating . flip (File "bewertung.txt") time . Just . LBS.toStrict parseRating' = either (\(_ :: SomeException) -> Nothing) Just . parseRating . flip (File "bewertung.txt") time . Just . LBS.toStrict
time = UTCTime systemEpochDay 0 time = UTCTime systemEpochDay 0
mRating rating = rating { ratingValues = mRating' $ ratingValues rating } mRating rating = rating { ratingValues = mRating' $ ratingValues rating }