Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 1794
Image: ubuntu2004
1
from lxml import etree
2
import lxml.html
3
import os
4
5
6
7
# USAGE NOTE: If this script is loaded from another script/notebook
8
# located in a different directory, use the following pattern
9
# to ensure XSL files are imported correctly as well.
10
# oldwd=os.getcwd()
11
# try: os.chdir("path/to"); load("main.sage")
12
# finally: os.chdir(oldwd)
13
HTML_TRANSFORM = etree.XSLT(etree.parse(os.path.join("xsl","html.xsl")))
14
LATEX_TRANSFORM = etree.XSLT(etree.parse(os.path.join("xsl","latex.xsl")))
15
QTI_TRANSFORM = etree.XSLT(etree.parse(os.path.join("xsl","qti.xsl")))
16
17
18
19
# XSL Helpers
20
def insert_object_into_element(obj,name,element):
21
if obj is False:
22
return None #skip generating element only when exactly False (not falsy)
23
se = etree.SubElement(element, name)
24
if isinstance(obj, list):
25
for item in obj:
26
insert_object_into_element(item,"item",se)
27
elif isinstance(obj, dict):
28
for key in obj.keys():
29
insert_object_into_element(obj[key],key,se)
30
else:
31
if isinstance(obj,str):
32
se.text = obj
33
else:
34
se.text = f" {latex(obj)} "
35
36
def dict_to_tree(data_dict):
37
tree = etree.Element("data")
38
for key in data_dict.keys():
39
insert_object_into_element(data_dict[key], key, tree)
40
return tree
41
42
43
44
# Generator helpers
45
def mi_vars(*latex_names, random_order=True):
46
"""
47
Given one or more `latex_names` of strings, returns a tuple
48
of Sage variables. `random_order` names them so that they appear
49
in expressions in a random order.
50
"""
51
stamp = randrange(100000,999999)
52
indices = list(range(len(latex_names)))
53
if random_order:
54
shuffle(indices)
55
import string
56
random_letter = choice(list(string.ascii_lowercase))
57
return (var(f"{random_letter}_mi_var_{stamp}_{indices[i]}", latex_name=name) for i, name in enumerate(latex_names))
58
59
def shuffled_equation(*terms):
60
"""
61
Represents the equation sum(terms)==0, but with terms shuffled randomly
62
to each side.
63
"""
64
new_equation = (SR(0)==0)
65
for term in terms:
66
if choice([True,False]):
67
new_equation += (SR(term)==0)
68
else:
69
new_equation += (0==-SR(term))
70
return new_equation*choice([-1,1])
71
72
def base64_graphic(obj, file_format="svg"):
73
"""
74
Generates Base64 encoding of the graphic in the requested file_format.
75
"""
76
if not isinstance(obj,Graphics):
77
raise TypeError("Only graphics may be encoded as base64")
78
if file_format not in ["svg", "png"]:
79
raise ValueError("Invalid file format")
80
filename = tmp_filename(ext=f'.{file_format}')
81
obj.save(filename)
82
with open(filename, 'rb') as f:
83
from base64 import b64encode
84
b64 = b64encode(f.read())
85
return b64
86
87
def data_url_graphic(obj, file_format="svg"):
88
"""
89
Generates Data URL representing the graphic in the requested file_format.
90
"""
91
b64 = base64_graphic(obj, file_format=file_format).decode('utf-8')
92
if file_format=="svg":
93
file_format = "svg+xml"
94
return f"data:image/{file_format};base64,{b64}"
95
96
def latex_system_from_matrix(matrix, variables="x", alpha_mode=False, variable_list=[]):
97
# Augment with zero vector if not already augmented
98
if not matrix.subdivisions()[1]:
99
matrix=matrix.augment(zero_vector(ZZ, len(matrix.rows())), subdivide=true)
100
num_vars = matrix.subdivisions()[1][0]
101
# Start using requested variables
102
system_vars = variable_list
103
# Conveniently add xyzwv if requested
104
if alpha_mode:
105
system_vars += list(var("x y z w v"))
106
# Finally fall back to x_n as needed
107
system_vars += [var(f"{variables}_{n+1}") for n in range(num_vars)]
108
# Build matrix
109
latex_output = "\\begin{matrix}\n"
110
for row in matrix.rows():
111
if row[0]!= 0:
112
latex_output += latex(row[0]*system_vars[0])
113
previous_terms = True
114
else:
115
previous_terms = False
116
for n,cell in enumerate(row[1:num_vars]):
117
latex_output += " & "
118
if cell < 0 and previous_terms:
119
latex_output += " - "
120
elif cell > 0 and previous_terms:
121
latex_output += " + "
122
latex_output += " & "
123
if cell != 0:
124
latex_output += latex(cell.abs()*system_vars[n+1])
125
if not previous_terms:
126
previous_terms = bool(cell!=0)
127
if not previous_terms:
128
latex_output += " 0 "
129
latex_output += " & = & "
130
latex_output += latex(row[num_vars])
131
latex_output += "\\\\\n"
132
latex_output += "\\end{matrix}"
133
return latex_output
134
135
136
137
# Exercise object
138
class Exercise:
139
def __init__(self, title=None, slug=None, generator=None, template=None, seed=None):
140
self.__title = title
141
self.__slug = slug
142
self.__generator = generator
143
self.__template = template
144
self.__seed = seed
145
146
def data_dict(self):
147
set_random_seed(self.__seed)
148
return self.__generator()
149
150
def data_tree(self):
151
return dict_to_tree(self.data_dict())
152
153
def template(self):
154
PREFIX = """<?xml version="1.0"?>
155
<xsl:stylesheet version="1.0"
156
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
157
<xsl:output method="xml"/>
158
<xsl:template match="/data">
159
"""
160
SUFFIX = """
161
</xsl:template>
162
</xsl:stylesheet>
163
"""
164
return etree.XSLT(etree.XML(PREFIX+self.__template+SUFFIX))
165
166
def pretext_tree(self):
167
transform = self.template()
168
tree = transform(self.data_tree()).getroot()
169
tree.xpath("/*")[0].attrib['checkit-seed'] = f"{self.__seed:04}"
170
tree.xpath("/*")[0].attrib['checkit-slug'] = str(self.__slug)
171
tree.xpath("/*")[0].attrib['checkit-title'] = str(self.__title)
172
return tree
173
174
def pretext(self):
175
return str(etree.tostring(self.pretext_tree(), pretty_print=True), encoding="UTF-8")
176
177
def html_tree(self):
178
transform = HTML_TRANSFORM
179
tree = transform(self.pretext_tree()).getroot()
180
return tree
181
182
def html(self):
183
return str(etree.tostring(self.html_tree(),pretty_print=True), 'UTF-8')
184
185
def latex(self):
186
transform = LATEX_TRANSFORM
187
return str(transform(self.pretext_tree()))
188
189
def qti_tree(self):
190
transform = QTI_TRANSFORM
191
tree = transform(self.pretext_tree()).getroot()
192
for mattextxml in tree.xpath("//mattextxml"):
193
mattext = etree.Element("mattext")
194
mattext.attrib['texttype'] = 'text/html'
195
mattext.text = lxml.html.tostring(lxml.html.fromstring(etree.tostring(mattextxml.find("*"),pretty_print=True)),pretty_print=True)
196
mattextxml.addnext(mattext)
197
return tree
198
199
def qti(self):
200
return str(etree.tostring(self.qti_tree(),pretty_print=True), 'UTF-8')
201
202
def dict(self):
203
return {
204
"seed": self.__seed,
205
"qti": self.qti(),
206
"pretext": self.pretext(),
207
"html": self.html(),
208
"tex": self.latex(),
209
}
210
211
def preview(self):
212
print("Data XML")
213
print("-----------")
214
print(str(etree.tostring(self.data_tree(), pretty_print=True), "UTF-8"))
215
print()
216
print("HTML source")
217
print("-----------")
218
print(self.html())
219
print()
220
print("LaTeX source")
221
print("------------")
222
print(self.latex())
223
print()
224
print("QTI source")
225
print("------------")
226
print(self.qti())
227
print()
228
print("PreTeXt source")
229
print("------------")
230
print(self.pretext())
231
232
233
# Exercises collection
234
class Outcome():
235
def __init__(self, title=None, slug=None, generator=None, template=None, amount=50, fixed=False, public=False):
236
self.__title = title
237
self.__slug = slug
238
self.__generator = generator
239
self.__template = template
240
if public:
241
start = 0
242
end = 1000
243
else:
244
start = 1000
245
end = 10000
246
if fixed:
247
self.__seeds = list(range(start,start+amount))
248
else:
249
set_random_seed()
250
self.__seeds = [randrange(start,end) for _ in range(amount)]
251
252
def list(self):
253
return [Exercise(self.__title,self.__slug,self.__generator,self.__template,seed) for seed in self.__seeds]
254
255
def dict(self):
256
return {
257
"title": self.__title,
258
"slug": self.__slug,
259
"exercises": [e.dict() for e in self.list()],
260
}
261
262
def qtibank_generic_tree(self,bank_title):
263
qtibank_tree = etree.fromstring("""<?xml version="1.0"?>
264
<questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
265
<objectbank>
266
<qtimetadata>
267
<qtimetadatafield/>
268
</qtimetadata>
269
</objectbank>
270
</questestinterop>""")
271
label = etree.SubElement(qtibank_tree.find("*/*/*"), "fieldlabel")
272
label.text = "bank_title"
273
entry = etree.SubElement(qtibank_tree.find("*/*/*"), "fieldentry")
274
entry.text = f"{bank_title} -- {self.__slug}"
275
return qtibank_tree
276
277
def outcome_csv_row(self,count,bank_slug,oid_suffix):
278
return [
279
f"checkit_{bank_slug}_{count:02}_{self.__slug}_{oid_suffix:06}",
280
"outcome",
281
f"{count:02}-{self.__slug}: {self.__title}",
282
"",
283
self.__slug,
284
"n_mastery",
285
"2",
286
"3",
287
"4",
288
"Exceeds Mastery",
289
"3",
290
"Meets Mastery",
291
"2",
292
"Near Mastery",
293
"1",
294
"Well Below Mastery",
295
"0",
296
"Insufficient Work to Assess",
297
]
298
299
def build_files(
300
self,
301
build_path="__build__",
302
bank_title="CheckIt Question Bank"
303
):
304
# provision filesystem
305
if not os.path.isdir(build_path): os.mkdir(build_path)
306
outcome_build_path = os.path.join(build_path, self.__slug)
307
if not os.path.isdir(outcome_build_path): os.mkdir(outcome_build_path)
308
qtibank_build_path = os.path.join(build_path, "qti-bank")
309
if not os.path.isdir(qtibank_build_path): os.mkdir(qtibank_build_path)
310
print(f"Building {outcome_build_path}...")
311
312
qtibank_tree = self.qtibank_generic_tree(bank_title)
313
314
for count,seed in enumerate(self.__seeds):
315
exercise = self.list()[count]
316
# build flat files
317
with open(f'{outcome_build_path}/{count:04}.ptx','w') as outfile:
318
print(exercise.pretext(), file=outfile)
319
with open(f'{outcome_build_path}/{count:04}.tex','w') as outfile:
320
print(exercise.latex(), file=outfile)
321
with open(f'{outcome_build_path}/{count:04}.html','w') as outfile:
322
print(exercise.html(), file=outfile)
323
with open(f'{outcome_build_path}/{count:04}.qti','w') as outfile:
324
print(exercise.qti(), file=outfile)
325
# add to qtibank file
326
qtibank_tree.find("*").append(exercise.qti_tree())
327
qtibank_tree.find("*").attrib['ident'] = self.__slug
328
with open(f'{qtibank_build_path}/{self.__slug}.qti','w') as outfile:
329
print(str(etree.tostring(qtibank_tree, encoding="UTF-8", xml_declaration=True,pretty_print=True),"UTF-8"), file=outfile)
330
print(f"- Files built successfully!")
331
332
333
334
# Bank building
335
def build_bank(bank_path, amount=50, fixed=False, public=False):
336
config = etree.parse(os.path.join(bank_path, "__bank__.xml"))
337
bank_title = config.find("title").text
338
bank_slug = config.find("slug").text
339
# build Canvas outcome CSV
340
outcome_csv = [[
341
"vendor_guid",
342
"object_type",
343
"title",
344
"description",
345
"display_name",
346
"calculation_method",
347
"calculation_int",
348
"mastery_points",
349
"ratings",
350
]]
351
# build JSON blob for bank
352
bank_json = {
353
"title": bank_title,
354
"slug": bank_slug,
355
"outcomes": [],
356
}
357
# Canvas chokes on repeated IDs from mult instructors in same institution
358
import time; oid_suffix = time.time()
359
for n,objective in enumerate(config.xpath("objectives/objective")):
360
slug = objective.find("slug").text
361
title = objective.find("title").text
362
oldwd=os.getcwd();os.chdir(bank_path)
363
load(f"{slug}.sage") # imports `generator` function
364
os.chdir(oldwd)
365
with open(os.path.join(bank_path, f"{slug}.ptx"),'r') as template_file:
366
template = template_file.read()
367
outcome = Outcome(
368
title=title,
369
slug=slug,
370
generator=generator,
371
template=template,
372
amount=amount,
373
fixed=fixed,
374
public=public,
375
)
376
outcome.build_files(
377
build_path=os.path.join(bank_path,"__build__"),
378
bank_title=bank_title,
379
)
380
bank_json["outcomes"].append(outcome.dict())
381
outcome_csv.append(outcome.outcome_csv_row(n,bank_slug,oid_suffix))
382
import csv
383
with open(os.path.join(bank_path, "__build__", f"{bank_slug}-canvas-outcomes.csv"),'w') as f:
384
csv.writer(f).writerows(outcome_csv)
385
print("Canvas outcomes built.")
386
import json
387
with open(os.path.join(bank_path, "__build__", f"{bank_slug}-bank.json"),'w') as f:
388
json.dump(bank_json,f)
389
print("JSON blob built.")
390
print("Bank build complete!")
391
392