remarks 0.1.9 → 0.1.10
raw patch · 13 files changed
+253/−77 lines, 13 filesdep +containers
Dependencies added: containers
Files
- README.md +29/−11
- app/Main.hs +32/−9
- remarks.cabal +3/−1
- src/Export/CSV.hs +4/−15
- src/Export/Generic.hs +14/−0
- src/Export/Html.hs +5/−5
- src/Invalid.hs +50/−3
- src/Parser/Impl.hs +2/−1
- src/Pending.hs +80/−0
- src/PointsChecker.hs +7/−6
- src/PrettyPrinter.hs +14/−14
- src/PropertyInterp.hs +10/−10
- test/PointsCheckerTests.hs +3/−2
README.md view
@@ -46,29 +46,47 @@ are correct. * `remarks show [<file>]` checks the file system structure as above, and shows the overall judgement for each given argument.-* `remarks summary <depth> [<file>]` checks and summarizes the points. Depth 0- lists just the top-level judgements.-+* `remarks summary [--depth <depth>] [<file>]` checks and summarizes the points.+ Depth 0 (default) lists just the top-level judgements.+* `remarks pending [--depth <depth>] [<file>]` shown the corrections that has not+ been completed. Can be cut at a given depth; depth 0 lists just the top-level judgements.+* `remarks export [--format "<format>"] [<file>]` exports corrections to a semicolon separated + list. The format is a semicolon separated string of properties.+* `remarks exportHTML [<file>]` exports all corrections to a dynamic html-table. Developer features: * `remarks parse [<file>]` parses the given files and shows their ASTs. ## Installation -[`remarks` is on Hackage](http://hackage.haskell.org/package/remarks), so you-can just use [Cabal](https://www.haskell.org/cabal/):+`remarks` is on [Hackage](http://hackage.haskell.org/package/remarks), so you+can use [Cabal](https://www.haskell.org/cabal/): ``` $ cabal install remarks ``` -You can also clone this repository and use-[Stack](https://docs.haskellstack.org/en/stable/README/):+If you clone the repository, or [download the+sources](https://github.com/oleks/remarks/releases), you get two further+options: -```-$ stack build-$ stack install-```+* If you are using the purely functional package manager+ [Nix](https://nixos.org/nix/), you can do this:++ ```+ $ nix-build+ ```++ This will create a symlink `result`, pointing to the directory in your Nix+ store, containing the binary.++* Or, if using [Stack](https://docs.haskellstack.org/en/stable/README/), you+ can do this:++ ```+ $ stack build+ $ stack install+ ``` ## Syntax
app/Main.hs view
@@ -6,6 +6,7 @@ import Parser import PointsChecker import PropertyInterp+import Pending import PrettyPrinter import Control.Monad ( liftM, (<=<) )@@ -134,20 +135,23 @@ check js = do case marshall js of Right newJs -> printJs newJs- Left e -> putStrLn $ show e+ Left e -> putStrLn $ reportInvalid e printJs :: [Judgement] -> IO () printJs = putStrLn . ppJs -export :: [String] -> [Judgement] -> IO ()+export :: String -> [Judgement] -> IO () export format js = do- case (exportCSV ";" format =<< (marshall js)) of+ case (exportCSV [delimiter] formatList =<< (marshall js)) of Right docs -> putStrLn docs- Left e -> putStrLn $ show e+ Left e -> putStrLn $ reportInvalid e+ where + delimiter = findDelimiter format+ formatList = splitBy delimiter format export_html :: [Judgement] -> IO () export_html js = do- case (mapM checkPoints js) of+ case (mapM interpProps js) of Right newJs -> putStrLn $ exportHTML newJs Left e -> putStrLn $ show e @@ -157,6 +161,19 @@ Right mJs -> printJs $ map (summary depth) mJs Left e -> putStrLn $ show e +pending :: Maybe Int -> [Judgement] -> IO ()+pending dl js = do+ case findPending dl js of+ Nothing -> putStrLn "No pending corrections."+ (Just s) -> putStrLn "The following corrections are pending:" >> putStrLn s++findDelimiter :: String -> Char+findDelimiter [] = ';' -- There is no delimiter so it doesn't matter+findDelimiter (s:ss) =+ if (elem s [',',';'])+ then s+ else findDelimiter ss+ main :: IO () main = do args <- getArgs@@ -168,12 +185,18 @@ with paths $ mapM_ check ("show" : paths) -> with paths $ mapM_ printJs- ("summary" : depth : paths) ->- with paths $ mapM_ $ showSummary (read depth)+ ("pending" : "--depth" : d : paths) ->+ with paths $ mapM_ (pending (Just $ read d))+ ("pending" : paths) ->+ with paths $ mapM_ (pending Nothing)+ ("summary" : "--depth" : d : paths) ->+ with paths $ mapM_ $ showSummary $ read d+ ("summary" : paths) ->+ with paths $ mapM_ $ showSummary 0 ("export" : "--format" : format : paths) ->- with paths $ mapM_ $ export (splitBy ';' format)+ with paths $ mapM_ $ export format ("export" : paths) ->- with paths $ mapM_ $ export ["Title", "Total", "MaxPoints"]+ with paths $ mapM_ $ export "Title;Total;MaxPoints" ("exportHTML" : paths) -> with paths $ mapM_ export_html (c:rest) -> invalidCommand c rest
remarks.cabal view
@@ -1,5 +1,5 @@ name: remarks-version: 0.1.9+version: 0.1.10 synopsis: A DSL for marking student work description: A DSL for marking student work; see README.md for further details. homepage: https://github.com/oleks/remarks#readme@@ -24,6 +24,7 @@ , Invalid , Parser , Parser.Impl+ , Pending , PointsChecker , PrettyPrinter , PropertyInterp@@ -31,6 +32,7 @@ build-depends: base >= 4.7 && < 5 , GenericPretty >= 1.2.1 , pretty >= 1.1.3.3+ , containers >= 0.5.7.1 default-language: Haskell2010 executable remarks
src/Export/CSV.hs view
@@ -18,18 +18,7 @@ doc <- mapM (mapfun judgement) properties pure $ hcat $ (intersperse (text delimiter)) doc where- mapfun = flip lookupProperty--lookupProperty :: String -> Judgement -> Either Invalid Doc-lookupProperty name (Judgement (_, properties, _, _)) =- case (lookup name (map (\(Property (n,v)) -> (n,v)) properties)) of- Nothing -> Left $ PropertyNotFound name- Just(value) -> pure $ formatPropertyExp value-lookupProperty name _ =- Left $ PropertyNotFound name--formatPropertyExp :: PropertyExp -> Doc-formatPropertyExp (Lookup (index, name)) =- brackets $ int index <> text "." <> text name-formatPropertyExp (Value value) = text value-formatPropertyExp (Num value) = pointsDoc value+ mapfun j p = + case lookupProperty p j of+ Nothing -> Left $ PropertyNotFound p j+ (Just v) -> pure v
src/Export/Generic.hs view
@@ -7,9 +7,23 @@ isIntegral x = x == fromInteger (round x) pointsDoc :: Double -> Doc+pointsDoc v | isInfinite v = empty pointsDoc v | isNaN v = empty pointsDoc v | isIntegral v = integer (round v) pointsDoc v = double v++lookupProperty :: String -> Judgement -> Maybe Doc+lookupProperty name (Judgement (_, properties, _, _)) =+ case (lookup name (map (\(Property (n,v)) -> (n,v)) properties)) of+ Nothing -> Nothing+ Just(value) -> pure $ formatPropertyExp value+lookupProperty _ _ = Nothing -- Bonus does not have properties++formatPropertyExp :: PropertyExp -> Doc+formatPropertyExp (Lookup (index, name)) =+ brackets $ int index <> text "." <> text name+formatPropertyExp (Value value) = text value+formatPropertyExp (Num value) = pointsDoc value unify :: Judgement -> Judgement -> Maybe Judgement unify
src/Export/Html.hs view
@@ -12,13 +12,13 @@ (body . table $ htmlTableHead (head js) $+$ vcat (map htmlJudgement js)) tag :: String -> Doc -> Doc -> Doc-tag tagStr attr doc = (text "<" <> text tagStr <+> attr <> text ">") $$ nest 2 doc $$ text ("</" ++ tagStr ++ ">")+tag tagStr attr doc = (text "<" <> text tagStr <> attr <> text ">") $$ nest 2 doc $$ text ("</" ++ tagStr ++ ">") etag :: String -> Doc -> Doc etag tagStr = tag tagStr empty atag :: String -> String -> Doc -> Doc-atag tagStr attrStr = tag tagStr (text attrStr)+atag tagStr attrStr = tag tagStr (space <> text attrStr) doctype :: Doc -> Doc doctype = ($$) (text "<!DOCTYPE html>")@@ -115,14 +115,14 @@ htmlDetailJudgement :: Judgement -> Doc htmlDetailJudgement (Bonus (points, comments)) =- details (pointsDoc points) (htmlDetailComments comments)+ details (text "Bonus" <+> parens (pointsDoc points)) (htmlDetailComments comments) htmlDetailJudgement (Judgement (Header(title, points, maxPoint), _, comments, judgements)) = details- (text title <> parens (pointsDoc points <> text "/" <> pointsDoc maxPoint))+ (text title <+> parens (pointsDoc points <> text "/" <> pointsDoc maxPoint)) (htmlDetailComments comments $$ htmlDetailJudgements judgements) htmlDetailComments :: [Comment] -> Doc-htmlDetailComments [] = text ""+htmlDetailComments [] = empty htmlDetailComments comments = ul . vcat $ map htmlDetailComment comments
src/Invalid.hs view
@@ -1,18 +1,65 @@ {-# LANGUAGE DeriveGeneric #-} -module Invalid where+module Invalid ( reportInvalid, Invalid (..) ) where import Ast+import PrettyPrinter +import Data.List (intersperse) import Text.PrettyPrint.GenericPretty data Invalid- = PointsExceedMaxPoints Header+ = PointsExceedMaxPoints Judgement | BadSubJudgementPointsSum Judgement | BadSubJudgementMaxPointsSum Judgement | NoPointsInBottomJudgement Judgement- | PropertyNotFound String+ | PropertyNotFound String Judgement deriving (Eq, Show, Generic) instance Out Invalid +reportInvalid :: Invalid -> String+reportInvalid (PointsExceedMaxPoints (j @ (Judgement ((Header (_, p, m)), _, _, _)))) =+ "The total points (" ++ ppPoints p ++ ") exceeded maximum (" ++ ppPoints m ++ ") in the judgement\n" +++ reportStrippedJudgement j+reportInvalid (BadSubJudgementPointsSum (j @ (Judgement (Header (_, p, _), _, _, _)))) =+ "The sum of points (" ++ ppPoints p ++ ") in judgement is not the sum of sub-judgements\n" +++ reportJudgement 0 j+reportInvalid (BadSubJudgementMaxPointsSum (j @ (Judgement (Header (_, _, m), _, _, _)))) =+ "The maximum points (" ++ ppPoints m ++ ") in judgement is not the sum of sub-judgements\n" +++ reportJudgement 0 j+reportInvalid (NoPointsInBottomJudgement j) = + "No points reported in leaf-judgement\n" ++ reportStrippedJudgement j+reportInvalid (PropertyNotFound s j) = + "Property " ++ s ++ " not found in judgement\n" ++ reportJudgement 0 j+reportInvalid m = "Cannot parse error message\n" ++ show m ++ "\nPlease report this message to someone!"++reportJudgement :: Int -> Judgement -> String+reportJudgement d j | isLeafJ j = ppJ_d d (stripJ j)+reportJudgement 1 j | isNodeJ j = (ppJ_d 1 $ stripJ j) ++ "\n ..."+reportJudgement 0 j | isNodeJ j = + (ppJ_d 0 $ stripJ j) ++ (concat $ intersperse "\n" (map (reportJudgement 1) (subJs j)))+reportJudgement _ _ = ""++reportStrippedJudgement :: Judgement -> String+reportStrippedJudgement j | isLeafJ j = reportJudgement 0 (stripJ j)+reportStrippedJudgement j | isNodeJ j = (reportJudgement 0 (stripJ j)) ++ "\n ..."+reportStrippedJudgement _ = "" -- Bonus++subJs :: Judgement -> [Judgement]+subJs (Judgement (_, _, _, js)) = js+subJs _ = [] -- Bunus++stripJ :: Judgement -> Judgement+stripJ (Judgement (h, _, _, _)) = Judgement (h, [], [], [])+stripJ b = b -- Bunus++isLeafJ :: Judgement -> Bool+isLeafJ (Judgement (_, _, _, [])) = True+isLeafJ (Judgement (_, _, _, (_:_))) = False+isLeafJ (Bonus _) = False++isNodeJ :: Judgement -> Bool+isNodeJ (Judgement (_, _, _, [])) = False+isNodeJ (Judgement (_, _, _, (_:_))) = True+isNodeJ (Bonus _) = False
src/Parser/Impl.hs view
@@ -38,7 +38,8 @@ parseMaxPoints :: ReadP Double parseMaxPoints = do is <- parseIntegral- case (maybeRead is) of+ fs <- (string ".5") +++ pure "0"+ case (maybeRead (is ++ "." ++ fs)) of Just x -> pure x _ -> pfail
+ src/Pending.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE DeriveGeneric #-}++module Pending ( findPending ) where++import Ast++import Data.Tree+import Text.PrettyPrint++type PendingTree = Tree String++data FormatTree+ = TEmpty FormatTree+ | TSpace FormatTree+ | TBranch FormatTree+ | TNode+ | TLeaf++showTree :: FormatTree -> Doc+showTree (TEmpty t) = showTree t+showTree (TBranch t) = text " | " <> showTree t+showTree (TSpace t) = text " " <> showTree t+showTree TNode = text " |- "+showTree TLeaf = text " |> "++linebreak :: Doc+linebreak = text "\n"++formatTree :: PendingTree -> Doc+formatTree t = formatSubTree (TEmpty) t++formatSubTrees :: (FormatTree -> FormatTree) -> [PendingTree] -> Doc+formatSubTrees _ [] = empty+formatSubTrees ft [(Node s [])] =+ showTree (ft TLeaf) <> text s+formatSubTrees ft [t] =+ showTree (ft TNode) <> formatSubTree (ft . TSpace) t+formatSubTrees ft ((Node s []):ts) =+ showTree (ft TLeaf) <> text s <> linebreak <>+ (formatSubTrees ft ts)+formatSubTrees ft (t:ts) =+ showTree (ft TNode) <> formatSubTree (ft . TBranch) t <> linebreak <>+ (formatSubTrees ft ts)++formatSubTree :: (FormatTree -> FormatTree) -> PendingTree -> Doc+formatSubTree _ (Node s []) =+ text s+formatSubTree tree (Node s ts) =+ text s <> linebreak <> formatSubTrees tree ts++size :: PendingTree -> Int+size (Node _ []) = 1+size (Node _ (t @ (_:_))) = sum $ map size t++limitPendingTree :: Int -> PendingTree -> PendingTree+limitPendingTree _ (Node t []) = (Node t [])+limitPendingTree 0 (Node t sub) = (Node (t ++ showTasks (sum $ map size sub)) [])+limitPendingTree n (Node t sub) = (Node t (map (limitPendingTree (n-1)) sub))++showTasks :: Int -> String+showTasks 1 = " (1 task)"+showTasks n = " (" ++ (show n) ++ " tasks)"++pendingJudgement :: Judgement -> [PendingTree]+pendingJudgement (Bonus (_, _)) = []+pendingJudgement (Judgement (Header (t, p, _), _, _, [])) | isInfinite p = [Node t []]+pendingJudgement (Judgement (Header (_, _, _), _, _, [])) = [] -- Not infinite+pendingJudgement (Judgement (Header (t, _, _), _, _, subJs @ (_:_))) =+ case (concatMap pendingJudgement subJs) of+ [] -> []+ sub -> [Node t sub]++findPending :: Maybe Int -> [Judgement] -> Maybe(String)+findPending detailLevel js =+ case (concatMap pendingJudgement js) of+ [] -> Nothing+ t ->+ case detailLevel of+ Nothing -> Just $ render $ vcat $ map formatTree t+ (Just i) -> Just $ render $ vcat $ map (formatTree . (limitPendingTree i)) t
src/PointsChecker.hs view
@@ -12,22 +12,22 @@ x ~= y = abs (x - y) <= 0.01 checkPointsSubJs :: Judgement -> Either Invalid Judgement-checkPointsSubJs (Judgement ((Header (t, _, maxP)), prop, cs, subJs)) = do+checkPointsSubJs (Judgement (Header (t, _, maxP), prop, cs, subJs)) = do newSubJs <- mapM checkPoints subJs let newP = sum $ map points newSubJs pure $ Judgement (Header (t, newP, maxP), prop, cs, newSubJs) checkPointsSubJs j = pure j checkPoints :: Judgement -> Either Invalid Judgement-checkPoints j @ (Judgement ((Header (_, p, _)), _, _, [])) | isInfinite p = do+checkPoints j @ (Judgement ((Header (_, p, _)), _, _, [])) | isInfinite p = Left $ NoPointsInBottomJudgement j-checkPoints j @ (Judgement ((Header (_, p, maxP)), _, _, subJs @ (_:_))) | isInfinite p = do+checkPoints j @ (Judgement (Header (_, p, maxP), _, _, subJs @ (_:_))) | isInfinite p = do try ((sum $ map maxPoints subJs) ~= maxP) (BadSubJudgementMaxPointsSum j) checkPointsSubJs j-checkPoints j @ (Judgement (h @ (Header (_, p, maxP)), _, _, subJs)) = do+checkPoints j @ (Judgement (Header (_, p, maxP), _, _, subJs)) = do try (p <= maxP)- (PointsExceedMaxPoints h)+ (PointsExceedMaxPoints j) case subJs of [] -> pure j _ -> do@@ -36,10 +36,11 @@ try ((sum $ map maxPoints subJs) ~= maxP) (BadSubJudgementMaxPointsSum j) checkPointsSubJs j-checkPoints j = pure j+checkPoints j @ (Bonus _) = pure j points :: Judgement -> Double points (Bonus (v, _)) = v+points (Judgement (Header (_, v, _), _, _, [])) | isInfinite v = 0 points (Judgement (Header (_, v, _), _, _, _)) = v maxPoints :: Judgement -> Double
src/PrettyPrinter.hs view
@@ -1,4 +1,4 @@-module PrettyPrinter (ppJs) where+module PrettyPrinter (ppJ_d, ppJs, ppPoints) where import Ast import Export.Generic@@ -6,33 +6,33 @@ import Text.PrettyPrint import Data.List (intersperse) +ppJ_d :: Int -> Judgement -> String+ppJ_d d = render . (formatJudgement (d + 1))+ ppJs :: [Judgement] -> String ppJs = render . vcat . intersperse (text "") . map (formatJudgement 1) +ppPoints :: Double -> String+ppPoints = render . pointsDoc+ formatJudgement :: Int -> Judgement -> Doc-formatJudgement level (Bonus (points, _)) =- (text $ replicate level '#') <+> text "Bonus" <> colon <+> text "+" <>+formatJudgement depth (Bonus (points, _)) =+ (text $ replicate depth '#') <+> text "Bonus" <> colon <+> text "+" <> pointsDoc points-formatJudgement level (Judgement (header, properties, comments, judgements)) =- formatHeader level header $+$+formatJudgement depth (Judgement (header, properties, comments, judgements)) =+ formatHeader depth header $+$ (nest 2 $ vcat $ map formatProperty properties) $+$ (nest 2 $ vcat $ map formatComment comments) $+$- (vcat $ map (formatJudgement (level + 1)) judgements)+ (vcat $ map (formatJudgement (depth + 1)) judgements) formatHeader :: Int -> Header -> Doc-formatHeader level (Header (title, point, maxPoints)) =- (text $ replicate level '#') <+> text title <> colon <> space <>+formatHeader depth (Header (title, point, maxPoints)) =+ (text $ replicate depth '#') <+> text title <> colon <> space <> pointsDoc point <> text "/" <> pointsDoc maxPoints formatProperty :: Property -> Doc formatProperty (Property (name, value)) = colon <> text name <> colon <+> formatPropertyExp value--formatPropertyExp :: PropertyExp -> Doc-formatPropertyExp (Lookup (index, name)) =- brackets $ int index <> text "." <> text name-formatPropertyExp (Value v) = text v-formatPropertyExp (Num v) = pointsDoc v formatComment :: Comment -> Doc formatComment (Comment (mood, commentParts)) =
src/PropertyInterp.hs view
@@ -24,11 +24,11 @@ Left invalid -> Left invalid interpJudgement :: Judgement -> Either Invalid (Judgement, [(String, PropertyValue)])-interpJudgement (Judgement (h, prop, cs, js)) = do+interpJudgement (j @ (Judgement (h, prop, cs, js))) = do updlist <- mapM interpJudgement js let (jupdates, jsPropVals) = unzip updlist predef <- generatePredefinedValues h- propVals <- mapM (bindProp (predef:jsPropVals)) (addPredifinedProps prop)+ propVals <- mapM (bindProp j (predef:jsPropVals)) (addPredifinedProps prop) let newProps = map propValToProperties propVals pure (Judgement (h, newProps, cs, jupdates), propVals) interpJudgement j @ (Bonus (v, _)) = pure (j, [("Bonus", DoubVal v)])@@ -47,18 +47,18 @@ generatePredefinedValues (Header (t, p, maxP)) = pure [("Title", StrVal t), ("Total", DoubVal p), ("MaxPoints", DoubVal maxP)] -bindProp :: [[(String, PropertyValue)]] -> Property ->+bindProp :: Judgement -> [[(String, PropertyValue)]] -> Property -> Either Invalid (String, PropertyValue)-bindProp propEnv (Property (name, propExp)) =- case (evalPropExp propExp propEnv) of+bindProp rj propEnv (Property (name, propExp)) =+ case (evalPropExp rj propExp propEnv) of Right (val) -> pure (name, val) Left invalid -> Left invalid -evalPropExp :: PropertyExp -> [[(String, PropertyValue)]] ->+evalPropExp :: Judgement -> PropertyExp -> [[(String, PropertyValue)]] -> Either Invalid PropertyValue-evalPropExp (Value s) _ = pure $ StrVal s-evalPropExp (Num n) _ = pure $ DoubVal n-evalPropExp (Lookup (i, p)) propEnv =+evalPropExp _ (Value s) _ = pure $ StrVal s+evalPropExp _ (Num n) _ = pure $ DoubVal n+evalPropExp rj (Lookup (i, p)) propEnv = case (lookup p (propEnv !! (i))) of- Nothing -> Left $ PropertyNotFound p+ Nothing -> Left $ PropertyNotFound p rj (Just s) -> pure s
test/PointsCheckerTests.hs view
@@ -45,10 +45,11 @@ ] negUnitTests :: TestTree-negUnitTests = testGroup "Positive Unit Tests"+negUnitTests = testGroup "Negative Unit Tests" [ testCase "Points exceed max points" $ checkPointsStr "# A: 1/0\n" @?=- [Left $ PointsExceedMaxPoints (Header ("A", 1.0, 0.0))]+ [Left $ PointsExceedMaxPoints $+ (Judgement (Header ("A", 1.0, 0.0), [], [], []))] , testCase "Sub-judgement points don't sum up to points" $ checkPointsStr "# A: 0/0\n## B: 1/0\n" @?= [Left $ BadSubJudgementPointsSum