Commit be54d03a authored by Henrik Tramberend's avatar Henrik Tramberend
Browse files

This might actually work.

parent f3293eba
......@@ -3,13 +3,16 @@
import Control.Monad ()
import Control.Exception
import Data.Maybe ()
import Data.List
import Data.Typeable
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.Text.Encoding as E
import qualified Data.Yaml as Y
import qualified Data.HashMap.Strict as Map
import Data.Yaml.Pretty as Y
import Data.Hashable
import Data.Maybe
import qualified Data.ByteString.Char8 as B
import Debug.Trace
import Development.Shake
......@@ -106,13 +109,153 @@ main = do
projectDir
(takeDirectory examPath)
(examStudentInfoFile exam)
Students hashMap <- readStudentInfo studentInfoPath (examTrack exam)
renderExam exam questions (Map.elems hashMap) out
Students studentMap <- readStudentInfo studentInfoPath (examTrack exam)
putNormal $ "Read student data from: " ++ studentInfoPath
let examData = generateExam exam questions (Map.elems studentMap)
putNormal $
"Exams generated for N students. N: " ++ (show . length . snd) examData
templates <- compileTemplates "exam"
putNormal "Templates compiled."
examPandoc <- compileExam projectDir templates examData
putNormal "Exams compiled to pandoc"
compilePandocPdf examPandoc out
--
phony
"clean" $
removeFilesAfter "." ["private"]
compileTemplates :: FilePath -> Action MT.TemplateCache
compileTemplates disposition = do
let templateNames =
[ "title-page.md"
, "student-title-page.md"
, "multiple-choice.md"
, "fill-text.md"
, "free-form.md"]
compiled <- mapM (compileProjectTemplate disposition) templateNames
return $ Map.fromList $ zip templateNames compiled
compileProjectTemplate :: FilePath -> FilePath -> Action MT.Template
compileProjectTemplate disposition name = do
projectDir <- getProjectDir
let filename = projectDir </> "exams" </> "templates" </> disposition </> name
need [filename]
text <- liftIO $ T.readFile $ filename
let result = M.compileTemplate name (fixMustacheMarkupText text)
return $
case result of
Right templ -> templ
Left parseError ->
throw $
MustacheException $ "Error parsing mustache template: " ++ (show parseError)
compilePandocPdf :: Pandoc -> FilePath -> Action ()
compilePandocPdf exam out = do
let variables =
[ ("fontsize", "12pt")
, ("fontfamily", "roboto")
, ("header-includes", "\\renewcommand{\\familydefault}{\\sfdefault}")]
let options =
def
{ writerStandalone = True
, writerVariables = variables
, writerTemplate = examLatexTemplate
, writerHighlight = True
, writerHighlightStyle = pygments
, writerCiteMethod = Citeproc
}
putNormal $ "# pandoc (for " ++ out ++ ")"
pandocMakePdf options exam out
compileQuestion :: FilePath -> MT.TemplateCache -> (Question, FilePath) -> Action Pandoc
compileQuestion projectDir templates question = do
return $ compileToPandoc question
where
compileToPandoc :: (Question, FilePath) -> Pandoc
compileToPandoc (quest,base) =
case readMarkdown def (renderMarkdown quest) of
Left err -> throw $ PandocException (show err)
Right pandoc -> walk (adjustImageUrls base) pandoc
adjustImageUrls base (Image attr inlines (url,title)) =
(Image attr inlines (adjustLocalUrl projectDir base url, title))
adjustImageUrls _ inline = inline
renderMarkdown question =
T.unpack $ M.substitute (chooseTemplate templates question) (MT.mFromJSON question)
compileToPandoc
:: Y.ToJSON a
=> MT.Template -> a -> Pandoc
compileToPandoc template thing =
case readMarkdown def $ T.unpack $ M.substitute template $ MT.mFromJSON thing of
Left err -> throw $ PandocException (show err)
Right pandoc -> pandoc
chooseTemplate :: MT.TemplateCache -> Question -> MT.Template
chooseTemplate templates question =
fromJust $
case qstAnswer question of
MultipleChoice _ -> Map.lookup "multiple-choice.md" templates
FillText _ _ -> Map.lookup "fill-text.md" templates
FreeForm _ _ -> Map.lookup "free-form.md" templates
lookupTemplate :: String -> MT.TemplateCache -> MT.Template
lookupTemplate name templates =
case Map.lookup name templates of
Just t -> t
Nothing -> throw $ MustacheException $ "Cannot lookup template: " ++ name
compileExam :: FilePath
-> MT.TemplateCache
-> (Exam, [(Student, [(Question, FilePath)])])
-> Action Pandoc
compileExam projectDir templates (exam,students) = do
let title = compileToPandoc (lookupTemplate "title-page.md" templates) exam
list <- mapM (compileStudentExam projectDir templates exam) students
return $ joinPandoc $ title : list
compileStudentExam :: FilePath
-> MT.TemplateCache
-> Exam
-> (Student, [(Question, FilePath)])
-> Action Pandoc
compileStudentExam projectDir templates exam (student,questions) = do
let title =
compileToPandoc (lookupTemplate "student-title-page.md" templates) $
StudentExam exam student
list <- mapM (compileQuestion projectDir templates) questions
return $ joinPandoc $ title : list
{-
compileExam :: (Exam, [(Student, [(Question, FilePath)])]) -> Action T.Text
compileExam exam = do
projectDir <- getProjectDir
let templateFile = "exam-template.md"
result <- liftIO $ M.automaticCompile [projectDir </> "test" </> "exams"] templateFile
let template =
case result of
Right templ -> templ
Left parseError ->
throw $
MustacheException $ "Error parsing mustache template: " ++ (show parseError)
return $ M.substitute template (MT.mFromJSON exam)
-}
{-
renderExam:: (Exam, [(Student, [Question])]) -> FilePath -> Action ()
renderExam exam pdfPath = do
titlePage <- renderTitlePage (fst exam) (length $ snd exam)
studentExams <- mapM (renderStudentExams $ fst exam) $ snd exam
renderExam exam pdfPath = do
frontPage = renderFrontPage exam
-}
joinPandoc
:: [Pandoc] -> Pandoc
joinPandoc list =
Pandoc nullMeta $
concatMap
(\(Pandoc _ blocks) ->
blocks)
list
-- | Filters questions by LectureIds and ExcludedTopicIds.
filterQuestions
:: [T.Text] -> [T.Text] -> [(Question, FilePath)] -> [(Question, FilePath)]
......@@ -129,21 +272,38 @@ groupQuestions questions =
where
groupBy attrib rmap question = Map.insertWith (++) (attrib $ fst question) [question] rmap
renderExam :: Exam -> [(Question, FilePath)] -> [Student] -> FilePath -> Action ()
renderExam exam questions students out = do
let candidates = filterQuestions (examLectureIds exam) (examExcludedTopicIds exam) questions
mapM_ (renderExamFor candidates) students
putNormal $ "Rendered exam: " ++ out
generateExam :: Exam
-> [(Question, FilePath)]
-> [Student]
-> (Exam, [(Student, [(Question, FilePath)])])
generateExam exam questions students =
let sorted = sortOn std_employeeNumber students
candidates = filterQuestions (examLectureIds exam) (examExcludedTopicIds exam) questions
studentQuestions = map (selectQuestionsForStudent candidates) sorted
in (exam, studentQuestions)
where
renderExamFor :: [(Question, FilePath)] -> Student -> Action ()
renderExamFor candidates student = do
selectQuestionsForStudent :: [(Question, FilePath)]
-> Student
-> (Student, [(Question, FilePath)])
selectQuestionsForStudent candidates student =
-- | Initialize the RNG with a hash over the student data.
-- Should produce the identical exam for the student each time.
-- Should produce the identical exam for one student each time and different
-- exams for all students every time.
let gen = mkStdGen (hash student)
let questions =
selection =
take (examNumberOfQuestions exam) $ shuffle' candidates (length candidates) gen
putNormal $ show (std_displayName student)
mapM_ (putNormal . show . qstLectureId . fst) questions
questions = map (shuffleChoices gen) selection
in (student, questions)
shuffleChoices gen (question,basePath) =
let answer =
case qstAnswer question of
MultipleChoice choices ->
MultipleChoice $ shuffle' choices (length choices) gen
_ -> qstAnswer question
in ( question
{ qstAnswer = answer
}
, basePath)
-- | Throw, result is shitty.
maybeThrowYaml
......@@ -180,12 +340,13 @@ readStudentInfo
readStudentInfo path track = do
need [path]
Students hashMap <- readYAML path
return $
Students $
Map.filter
(\s ->
std_track s == track)
hashMap
let trackStudents =
Map.filter
(\s ->
std_track s == track)
hashMap
putNormal $ "Students in track " ++ show track ++ ": " ++ show (length trackStudents)
return $ Students trackStudents
-- Reads all the questions and returns them along with the base directory of
-- each.
......@@ -267,7 +428,9 @@ shuffleAnswers q =
examStationary :: Exam
examStationary =
Exam
{ examStudentInfoFile = "PATH/TO/STUDENT/INFO"
{ examModule = "MODULE"
, examTitle = "TITLE"
, examStudentInfoFile = "PATH/TO/STUDENT/INFO"
, examDateTime = "DATE_TIME"
, examDurationInMinutes = 0
, examNumberOfQuestions = 0
......
# Aufgabe N: {{Title}}
| | |
|---------------|----------------|
| Titel | **{{Title}}** |
| Id | {{TopicId}} |
| Base | {{BaseDir}} |
| Vorlesung | {{LectureId}} |
| Schwierigkeit | {{Difficulty}} |
| Punkte | {{Points}} |
| Kommentar | {{Comment}} |
{{Question}}
{{Answer.FillText}}
*Antwort:* {{\#Answer.CorrectWords}}{{.}}, {{/Answer.CorrectWords}}
# Aufgabe N: {{Title}}
| | |
|---------------|----------------|
| Titel | **{{Title}}** |
| Id | {{TopicId}} |
| Base | {{BaseDir}} |
| Vorlesung | {{LectureId}} |
| Schwierigkeit | {{Difficulty}} |
| Punkte | {{Points}} |
| Kommentar | {{Comment}} |
{{Question}}
\fbox{\begin{minipage}{\textwidth} \hfill \vspace{ {{Answer.HeightInMm}}mm } \end{minipage}}
*Antwort:*
\fbox{\begin{minipage}{\textwidth} {{Answer.CorrectAnswer}} \end{minipage}}
# Aufgabe N: {{Title}}
| | |
|---------------|----------------|
| Titel | **{{Title}}** |
| Id | {{TopicId}} |
| Base | {{BaseDir}} |
| Vorlesung | {{LectureId}} |
| Schwierigkeit | {{Difficulty}} |
| Punkte | {{Points}} |
| Kommentar | {{Comment}} |
{{Question}}
*Antworten:*
{{\#Answer.Choices}}
- {{\#Correct}}$\boxtimes${{/Correct}}{{\^Correct}}$\square${{/Correct}} {{TheAnswer}}
{{/Answer.Choices}}
# Exam Template Test
Date: {{DataTime}}
\newpage
# Aufgabe: {{Title}}
{{Question}}
{{Answer.FillText}}
\newpage
# Aufgabe: {{Title}}
{{Question}}
\fbox{\begin{minipage}{\textwidth} \hfill \vspace{ {{Answer.HeightInMm}}mm } \end{minipage}}
\newpage
# Aufgabe: {{Title}}
{{Question}}
{{\#Answer.Choices}}
- $\square$ {{TheAnswer}}
{{/Answer.Choices}}
\newpage
# \Huge {{Student.displayName}} ({{Student.employeeNumber}})
# {{Exam.Title}}
## {{Exam.Module}}
Datum: {{Exam.DateTime}}\
Dauer: {{Exam.DurationInMinutes}} min\
Aufgaben: {{Exam.NumberOfQuestions}}\
Zug: {{Student.Track}}
## Auswertung
Punkte:
\setcounter{page}{1}
# {{Title}}
## {{Module}}
Datum: {{DateTime}}\
Dauer: {{DurationInMinutes}} min\
Aufgaben: {{NumberOfQuestions}}\
Zug: {{Track}}
# Aufgabe N: {{Title}}
| | |
|---------------|----------------|
| Titel | **{{Title}}** |
| Id | {{TopicId}} |
| Base | {{BaseDir}} |
| Vorlesung | {{LectureId}} |
| Schwierigkeit | {{Difficulty}} |
| Punkte | {{Points}} |
| Kommentar | {{Comment}} |
{{Question}}
{{Answer.FillText}}
*Antwort:* {{\#Answer.CorrectWords}}{{.}}, {{/Answer.CorrectWords}}
# Aufgabe N: {{Title}}
| | |
|---------------|----------------|
| Titel | **{{Title}}** |
| Id | {{TopicId}} |
| Base | {{BaseDir}} |
| Vorlesung | {{LectureId}} |
| Schwierigkeit | {{Difficulty}} |
| Punkte | {{Points}} |
| Kommentar | {{Comment}} |
{{Question}}
\fbox{\begin{minipage}{\textwidth} \hfill \vspace{ {{Answer.HeightInMm}}mm } \end{minipage}}
*Antwort:*
\fbox{\begin{minipage}{\textwidth} {{Answer.CorrectAnswer}} \end{minipage}}
# Aufgabe N: {{Title}}
| | |
|---------------|----------------|
| Titel | **{{Title}}** |
| Id | {{TopicId}} |
| Base | {{BaseDir}} |
| Vorlesung | {{LectureId}} |
| Schwierigkeit | {{Difficulty}} |
| Punkte | {{Points}} |
| Kommentar | {{Comment}} |
{{Question}}
*Antworten:*
{{\#Answer.Choices}}
- {{\#Correct}}$\boxtimes${{/Correct}}{{\^Correct}}$\square${{/Correct}} {{TheAnswer}}
{{/Answer.Choices}}
# Exam Template Test
Date: {{DataTime}}
# Exam Template Test
Date: {{DataTime}}
Track: 1
Module: MODULE
Title: TITLE
NumberOfQuestions: 6
LectureIds:
- LECTURE_ID
......@@ -6,4 +8,6 @@ ExcludedTopicIds:
- EXCLUDED_TOPIC_ID
DurationInMinutes: 30
StudentInfoFile: student-test-data.yaml
DateTime: DATE_TIME
DateTime: 22.11.2016 8:00
\documentclass[$if(fontsize)$$fontsize$,$endif$$if(lang)$$babel-lang$,$endif$$if(papersize)$$papersize$paper,$endif$$for(classoption)$$classoption$$sep$,$endfor$]{$documentclass$}
$if(fontfamily)$
\usepackage[$for(fontfamilyoptions)$$fontfamilyoptions$$sep$,$endfor$]{$fontfamily$}
$else$
\usepackage{lmodern}
$endif$
$if(linestretch)$
\usepackage{setspace}
\setstretch{$linestretch$}
$endif$
\usepackage{amssymb,amsmath}
\usepackage{ifxetex,ifluatex}
\usepackage{fixltx2e} % provides \textsubscript
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\usepackage[$if(fontenc)$$fontenc$$else$T1$endif$]{fontenc}
\usepackage[utf8]{inputenc}
$if(euro)$
\usepackage{eurosym}
$endif$
\else % if luatex or xelatex
\ifxetex
\usepackage{mathspec}
\else
\usepackage{fontspec}
\fi
\defaultfontfeatures{Ligatures=TeX,Scale=MatchLowercase}
$if(euro)$
\newcommand{\euro}{}
$endif$
$if(mainfont)$
\setmainfont[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$}
$endif$
$if(sansfont)$
\setsansfont[$for(sansfontoptions)$$sansfontoptions$$sep$,$endfor$]{$sansfont$}
$endif$
$if(monofont)$
\setmonofont[Mapping=tex-ansi$if(monofontoptions)$,$for(monofontoptions)$$monofontoptions$$sep$,$endfor$$endif$]{$monofont$}
$endif$
$if(mathfont)$
\setmathfont(Digits,Latin,Greek)[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
$endif$
$if(CJKmainfont)$
\usepackage{xeCJK}
\setCJKmainfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
$endif$
\fi
% use upquote if available, for straight quotes in verbatim environments
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
% use microtype if available
\IfFileExists{microtype.sty}{%
\usepackage{microtype}
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
}{}
$if(geometry)$
\usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry}
$endif$
\usepackage{hyperref}
$if(colorlinks)$
\PassOptionsToPackage{usenames,dvipsnames}{color} % color is loaded by hyperref
$endif$
\hypersetup{unicode=true,
$if(title-meta)$
pdftitle={$title-meta$},
$endif$
$if(author-meta)$
pdfauthor={$author-meta$},
$endif$
$if(keywords)$
pdfkeywords={$for(keywords)$$keywords$$sep$; $endfor$},
$endif$
$if(colorlinks)$
colorlinks=true,
linkcolor=$if(linkcolor)$$linkcolor$$else$Maroon$endif$,
citecolor=$if(citecolor)$$citecolor$$else$Blue$endif$,
urlcolor=$if(urlcolor)$$urlcolor$$else$Blue$endif$,
$else$
pdfborder={0 0 0},
$endif$
breaklinks=true}
\urlstyle{same} % don't use monospace font for urls
$if(lang)$
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\usepackage[shorthands=off,$for(babel-otherlangs)$$babel-otherlangs$,$endfor$main=$babel-lang$]{babel}
$if(babel-newcommands)$
$babel-newcommands$
$endif$
\else
\usepackage{polyglossia}
\setmainlanguage[$polyglossia-lang.options$]{$polyglossia-lang.name$}
$for(polyglossia-otherlangs)$
\setotherlanguage[$polyglossia-otherlangs.options$]{$polyglossia-otherlangs.name$}
$endfor$
\fi
$endif$
$if(natbib)$
\usepackage{natbib}
\bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$}
$endif$
$if(biblatex)$
\usepackage$if(biblio-style)$[style=$biblio-style$]$endif${biblatex}
$if(biblatexoptions)$\ExecuteBibliographyOptions{$for(biblatexoptions)$$biblatexoptions$$sep$,$endfor$}$endif$
$for(bibliography)$
\addbibresource{$bibliography$}
$endfor$
$endif$
$if(listings)$
\usepackage{listings}
$endif$
$if(lhs)$
\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
$endif$
$if(highlighting-macros)$
$highlighting-macros$
$endif$
$if(verbatim-in-note)$
\usepackage{fancyvrb}
\VerbatimFootnotes % allows verbatim text in footnotes
$endif$
$if(tables)$
\usepackage{longtable,booktabs}
$endif$
$if(graphics)$
\usepackage{graphicx,grffile}
\makeatletter
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
\makeatother
% Scale images if necessary, so that they will not overflow the page
% margins by default, and it is still possible to overwrite the defaults
% using explicit options in \includegraphics[width, height, ...]{}
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
$endif$
$if(links-as-notes)$
% Make links footnotes instead of hotlinks:
\renewcommand{\href}[2]{#2\footnote{\url{#1}}}
$endif$
$if(strikeout)$
\usepackage[normalem]{ulem}
% avoid problems with \sout in headers with hyperref:
\pdfstringdefDisableCommands{\renewcommand{\sout}{}}
$endif$
$if(indent)$
$else$
\IfFileExists{parskip.sty}{%
\usepackage{parskip}
}{% else
\setlength{\parindent}{0pt}
\setlength{\parskip}{6pt plus 2pt minus 1pt}
}
$endif$
\setlength{\emergencystretch}{3em} % prevent overfull lines
\providecommand{\tightlist}{%
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
$if(numbersections)$
\setcounter{secnumdepth}{5}
$else$
\setcounter{secnumdepth}{0}
$endif$
$if(subparagraph)$
$else$
% Redefines (sub)paragraphs to behave more like sections
\ifx\paragraph\undefined\else