-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfast-exam
executable file
·211 lines (180 loc) · 9.55 KB
/
fast-exam
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/usr/bin/env python3
from jinja2 import Environment, FileSystemLoader
from string import ascii_uppercase
import argparse
import random
import math
import time
import glob
import yaml
import os
import re
strings = {"IT": {"name": "Nome e Cognome", "id": "Matricola",
"date": "Data", "version": "Fila",
"instructions": "Istruzioni", "solution": "Soluzione",
"professor": "Professore", "course":"Corso"},
"EN": {"name": "First and Last Name", "id": "Student ID",
"date": "Date", "version": "Version",
"instructions": "Instructions", "solution": "Solution",
"professor": "Professor", "course":"Course"}
}
DEFAULT_HEIGHT="5cm"
parser = argparse.ArgumentParser(description='Create randomized exams based on a bank of exercises')
parser_1 = parser.add_argument_group('Exam Heading')
parser_1.add_argument('--date', type=str, default='December 17, 2023', help='The date of the exam')
parser_1.add_argument('--course', type=str, default='Operating Systems', help='The name of the course')
parser_1.add_argument('--professor', type=str, default='Martino Trevisan', help='The name of the professor')
parser_1.add_argument('--institution', type=str, default='Università di Trieste', help='The name of the university/institution')
parser_1.add_argument('--versions', type=int, default=2, help='How many versions of the exam to create')
parser_1.add_argument('--disclaimer', type=str, default="For each open question, please clearly write any assumption you make. Write in the given box using a comprehensible handwriting.", help='Instructions for the exam to print in the PDF') # Italian version: Per ogni domanda aperta, nel caso si ritenga che manchino delle informazioni o ipotesi necessarie, le si esplicitino nello svolgimento. Si scriva nel riquadro, usando una calligrafia e una dimensione del testo che permettano una lettura agevole.
parser_2 = parser.add_argument_group('Exam Building')
parser_2.add_argument('--db', type=str, default='sample-db', help='The folder of the quiz DB')
parser_2.add_argument('--structure', type=str, default="general:1,bash:1,open:1,exercise:1", help='How to sample the DB. Must be a list of group:n entries, separated by comma. E.g., general:1,bash:1,open:1')
parser_2.add_argument('--outdir', type=str, default="output", help='Where to save PDFs')
parser_2.add_argument('--lang', type=str, default='EN', help='Language oh headings. Supported IT and EN')
parser_2.add_argument('--seed', type=int, default=time.time(), help='Seed of the random package')
parser_2.add_argument('--template', type=str, default="template/template.tex", help='Template to use. Defaults to "template/template.tex"')
parser_2.add_argument('--keeptex', action='store_true', help='Keep Latex source for further editing')
parser_3 = parser.add_argument_group('Question Options')
parser_3.add_argument('--randomorder', action='store_true', help='Randomize the order of questions')
parser_3.add_argument('--solutionsummary', action='store_true', help='Print summary of the correct answers to quiz')
parser_3.add_argument('--quizbox', action='store_true', help='Print a box where students must write answers to quiz, to ease the correction')
parser_3.add_argument('--quizboxend', action='store_true', help='Print a box at the end of the document where students can write quiz answers and cut it to take home the quiz string.')
parser_3.add_argument('--quizbegin', action='store_true', help='Put all quiz before open questions')
parser_3.add_argument('--quizrandom', action='store_true', help='Randomize quiz answer order')
# Set globals
globals().update(vars(parser.parse_args()))
random.seed(seed)
def main():
# Check lang is supported
if not lang in strings:
print(f"Language {lang} not supported.")
return 1
# Load DB
questions = {}
for f in glob.glob(f"{db}/*.yml"):
section = os.path.basename(f).split(".")[0]
questions[section] = yaml.safe_load(open(f,"r"))
# Parse structure
parts =[]
for part in structure.split(","):
section, count = part.split(":")
# Handle Cherry Pick questions, starting by "+"
if count.startswith("+"):
count = [ int(e) for e in count.split("+")[1:] ]
parts.append( (section,count) )
else:
parts.append( (section,int(count)) )
# Choose questions
selected = []
for section, count in parts:
# Sample
if type(count)==int:
selected += random.sample(questions[section],count)
# Cherry Pick
elif type(count)==list:
for i in count:
selected.append(questions[section][i])
# Default height
for question in selected:
if question["type"] in {"open", "exercise"} and "height" not in question:
question["height"] = DEFAULT_HEIGHT
# Code style for solutions
for question in selected:
if question["type"] == "open" and "codestyle" in question and question["codestyle"]:
fontsize=question.get("solutionfontsize", 10)
question["solution"] = r"\setstretch{" + str(float(fontsize)/64) + r"}\texttt{" + \
r"{\fontsize{" + str(fontsize) + r"}{"+ str(fontsize) + r"}\selectfont " + \
tex_escape(question["solution"]) + r"}}"
# Make output directory
os.makedirs(outdir, exist_ok=True)
# Create versions
for letter in ascii_uppercase[:versions]:
# Randomize order
if randomorder:
random.shuffle(selected)
# Randomize answers to quiz
if quizrandom:
for q in selected:
if q["type"]=="quiz":
correct_sol = q["options"][q["solution"]]
random.shuffle(q["options"])
q["solution"] = q["options"].index(correct_sol)
# Quiz first if
9159
quizbegin is set
selected_quiz = []
selected_other = []
if quizbegin:
for q in selected:
if q["type"]=="quiz":
selected_quiz.append(q)
else:
selected_other.append(q)
selected = selected_quiz
selected += selected_other
# Invoke Exercises
for question in selected:
if question["type"]=="exercise":
ns = {}
exec (question["handler"], ns)
text = ns["handler"]()
question["question"] = text["question"]
if "solution" in text:
question["solution"] = text["solution"]
# Create the environment
environment = Environment(loader=FileSystemLoader(os.path.dirname(template)))
template_obj = environment.get_template(os.path.basename(template))
# Exam Text
content = template_obj.render(date=date, course=course, disclaimer=disclaimer,
professor=professor, institution=institution,
version = letter, questions = selected,
solution=False, solutionsummary=solutionsummary,
quizbox=quizbox, quizboxend=quizboxend,
strings=strings[lang])
print(content, file=open(f"{outdir}/test-{date}-vers-{letter}.tex", "w"))
os.system(f"pdflatex -output-directory='{outdir}' '{outdir}/test-{date}-vers-{letter}.tex'")
# Clean Up
if keeptex:
os.system(f"rm '{outdir}/test-{date}-vers-{letter}.log' '{outdir}/test-{date}-vers-{letter}.aux'")
else:
os.system(f"rm '{outdir}/test-{date}-vers-{letter}.log' '{outdir}/test-{date}-vers-{letter}.aux' '{outdir}/test-{date}-vers-{letter}.tex'")
# Solution
content = template_obj.render(date=date, course=course, disclaimer=disclaimer,
professor=professor, institution=institution,
version = letter, questions = selected,
solution=True, solutionsummary=solutionsummary,
quizbox=quizbox, quizboxend=quizboxend,
strings=strings[lang])
print(content, file=open(f"{outdir}/test-{date}-vers-{letter}-solution.tex", "w"))
os.system(f"pdflatex -output-directory='{outdir}' '{outdir}/test-{date}-vers-{letter}-solution.tex'")
# Clean Up
if keeptex:
os.system(f"rm '{outdir}/test-{date}-vers-{letter}-solution.log' '{outdir}/test-{date}-vers-{letter}-solution.aux'")
else:
os.system(f"rm '{outdir}/test-{date}-vers-{letter}-solution.log' '{outdir}/test-{date}-vers-{letter}-solution.aux' '{outdir}/test-{date}-vers-{letter}-solution.tex'")
import re
def tex_escape(text):
"""
:param text: a plain text message
:return: the message escaped to appear correctly in LaTeX
"""
print(text)
conv = {
'&': r'\&',
'%': r'\%',
'$': r'\$',
'#': r'\#',
'_': r'\_',
'{': r'\{',
'}': r'\}',
'~': r'\textasciitilde{}',
'^': r'\^{}',
'\\': r'\textbackslash{}',
'<': r'\textless{}',
'>': r'\textgreater{}',
}
regex = re.compile('|'.join(re.escape(str(key)) for key in sorted(conv.keys(), key = lambda item: - len(item))))
text = regex.sub(lambda match: conv[match.group()], text)
text = text.replace('\n','\\\\~\\\\').replace(" ", r"\hspace*{1.0ex}")
return text
if __name__ == "__main__":
main()