Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 165
Image: ubuntu2004
1
import lxml.etree, lxml.html
2
import os, json, subprocess, time, csv, io, zipfile
3
from IPython.core.display import display, HTML
4
5
TRANSFORM = {
6
filetype: lxml.etree.XSLT(lxml.etree.parse(os.path.join("xsl",f"{filetype}.xsl")))
7
for filetype in ["html","latex","qti"]
8
}
9
10
11
12
# XSL Helpers
13
def insert_object_into_element(obj,name,element):
14
"""
15
Inserts Python object into tree
16
"""
17
if obj is False:
18
return None #skip generating element only when exactly False (not falsy)
19
se = lxml.etree.SubElement(element, name)
20
if isinstance(obj, list):
21
for item in obj:
22
insert_object_into_element(item,"item",se)
23
elif isinstance(obj, dict):
24
for key in obj.keys():
25
insert_object_into_element(obj[key],key,se)
26
else:
27
se.text = str(obj)
28
29
def dict_to_tree(data_dict,seed):
30
"""
31
Takes a dictionary of data (typically randomized exercise data)
32
and represents it as an XML tree
33
"""
34
data = lxml.etree.Element("data")
35
data.attrib['seed'] = f"{seed:04}"
36
for key in data_dict.keys():
37
insert_object_into_element(data_dict[key], key, data)
38
return data
39
40
41
42
# Banks contain many Outcomes generate many Exercises
43
44
class Bank():
45
def __init__(self, slug=None):
46
# read manifest for bank
47
xml = lxml.etree.parse(os.path.join("banks",slug,"bank.xml")).getroot()
48
self.title = xml.find("title").text
49
self.slug = xml.find("slug").text
50
self.author = xml.find("author").text
51
self.url = xml.find("url").text
52
# create each outcome
53
self.outcomes = [
54
Outcome(
55
ele.find("title").text,
56
ele.find("slug").text,
57
ele.find("description").text,
58
ele.find("alignment").text,
59
self,
60
)
61
for ele in xml.xpath("outcomes/outcome")
62
]
63
64
def build_path(self,public=False,regenerate=False):
65
if not(regenerate):
66
try:
67
return self.__build_path
68
except:
69
pass
70
if public:
71
build_dir = "public"
72
else:
73
build_dir = time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime())
74
self.__build_path = os.path.join("banks",self.slug,"builds",build_dir)
75
os.makedirs(self.__build_path, exist_ok=True)
76
return self.__build_path
77
78
def generate_dict(self,public=False,amount=300,regenerate=False):
79
if public:
80
exs = "public exercises"
81
else:
82
exs = "private exercises"
83
print(f"Generating {exs} for {len(self.outcomes)} outcomes...")
84
olist = [o.generate_dict(public,amount,regenerate) for o in self.outcomes]
85
print("Exercises successfully generated for all outcomes!")
86
return {
87
"title": self.title,
88
"slug": self.slug,
89
"outcomes": olist,
90
}
91
92
def write_json(self,public=False,amount=300,regenerate=False):
93
build_path = self.build_path(public,regenerate)
94
with open(os.path.join(build_path, f"{self.slug}-bank.json"),'w') as f:
95
json.dump(self.generate_dict(public,amount,regenerate),f)
96
print(f"Bank JSON written to {build_path}")
97
98
def outcome_csv_list(self):
99
outcome_csv = [[
100
"vendor_guid",
101
"object_type",
102
"title",
103
"description",
104
"display_name",
105
"calculation_method",
106
"calculation_int",
107
"mastery_points",
108
"ratings",
109
]]
110
oid_suffix = time.time()
111
for count,outcome in enumerate(self.outcomes):
112
outcome_csv.append(outcome.csv_row(count,oid_suffix))
113
return outcome_csv
114
115
def write_outcome_csv(self,public=False,regenerate=False):
116
build_path = self.build_path(public)
117
with open(os.path.join(build_path, f"{self.slug}-canvas-outcomes.csv"),'w') as f:
118
csv.writer(f).writerows(self.outcome_csv_list())
119
print(f"Outcome CSV written to {build_path}")
120
121
def outcome_from_slug(self,outcome_slug):
122
return [x for x in self.outcomes if x.slug==outcome_slug][0]
123
124
def sample_for_outcome(self,outcome_slug):
125
return self.outcome_from_slug(outcome_slug).generate_exercises(amount=1,regenerate=True,save=False)[0]
126
127
def write_qti_zip(self,public=False,amount=300,regenerate=False):
128
build_path = self.build_path(public)
129
zip_buffer = io.BytesIO()
130
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
131
for outcome in self.outcomes:
132
zip_file.writestr(
133
f"{outcome.slug}.qti",
134
str(lxml.etree.tostring(outcome.qtibank_tree(public,amount,regenerate),
135
encoding="UTF-8", xml_declaration=True),"UTF-8")
136
)
137
with open(os.path.join(build_path, f"{self.slug}-canvas-qtibank.zip"),'wb') as f:
138
f.write(zip_buffer.getvalue())
139
print(f"Canvas QTI bank zip written to {build_path}")
140
141
def build(self,public=False,amount=300,regenerate=False):
142
self.write_json(public,amount,regenerate)
143
self.write_qti_zip(public,amount,regenerate)
144
self.write_outcome_csv(public,regenerate)
145
146
147
class Outcome():
148
def __init__(self, title=None, slug=None, description=None, alignment=None, bank=None):
149
self.title = title
150
self.slug = slug
151
self.description = description
152
self.alignment = alignment
153
self.bank = bank
154
155
def template_filepath(self):
156
return os.path.join(
157
"banks",
158
self.bank.slug,
159
"outcomes",
160
f"{self.slug}.ptx"
161
)
162
163
def template(self):
164
with open(self.template_filepath()) as template_file:
165
template_file_text = template_file.read()
166
complete_template = f"""<?xml version="1.0"?>
167
<xsl:stylesheet version="1.0"
168
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
169
<xsl:output method="xml"/>
170
<xsl:template match="/data">
171
{template_file_text}
172
</xsl:template>
173
</xsl:stylesheet>
174
"""
175
return lxml.etree.XSLT(lxml.etree.XML(complete_template))
176
177
def generator_filepath(self):
178
return os.path.join(
179
"banks",
180
self.bank.slug,
181
"outcomes",
182
f"{self.slug}.sage"
183
)
184
185
def generate_exercises(self,public=False,amount=300,regenerate=False,save=True):
186
if not(regenerate):
187
try:
188
return self.__exercises
189
except:
190
pass
191
# get sage script to run generator
192
script_path = os.path.join("scripts","generator.sage")
193
# run script to return JSON output with [amount] seeds
194
command = ["sage",script_path,self.generator_filepath(),str(amount)]
195
if public:
196
command.append("PUBLIC")
197
amount = 1000
198
else:
199
command.append("PRIVATE")
200
if public:
201
exs = "public exercises"
202
else:
203
exs = "private exercises"
204
print(f"Generating {amount} {exs} for {self.slug}...",end=" ")
205
# returns json list of exercise objects
206
data_json_list = subprocess.run(command,capture_output=True).stdout
207
print("Done!")
208
data_list = json.loads(data_json_list)
209
exercises = [
210
Exercise(data["values"],data["seed"],self) \
211
for data in data_list
212
]
213
if save:
214
self.__exercises = exercises
215
return exercises
216
217
def generate_dict(self,public=False,amount=300,regenerate=False):
218
exercises = self.generate_exercises(public,amount,regenerate)
219
return {
220
"title": self.title,
221
"slug": self.slug,
222
"description": self.description,
223
"alignment": self.alignment,
224
"exercises": [e.dict() for e in exercises],
225
}
226
227
def qtibank_tree(self,public=False,amount=300,regenerate=False):
228
qtibank_tree = lxml.etree.fromstring(f"""<?xml version="1.0"?>
229
<questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2"
230
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
231
xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
232
<objectbank ident="{self.bank.slug}_{self.slug}">
233
<qtimetadata>
234
<qtimetadatafield/>
235
</qtimetadata>
236
</objectbank>
237
</questestinterop>""")
238
label = lxml.etree.SubElement(qtibank_tree.find("*/*/*"), "fieldlabel")
239
label.text = "bank_title"
240
entry = lxml.etree.SubElement(qtibank_tree.find("*/*/*"), "fieldentry")
241
entry.text = f"{self.bank.title} -- {self.slug}"
242
for exercise in self.generate_exercises(public,amount,regenerate):
243
qtibank_tree.find("*").append(exercise.qti_tree())
244
return qtibank_tree
245
246
def csv_row(self,count,oid_suffix):
247
return [
248
f"checkit_{self.bank.slug}_{count:02}_{self.slug}_{oid_suffix:06}",
249
"outcome",
250
f"{count:02}-{self.slug}: {self.title}",
251
"",
252
self.slug,
253
"n_mastery",
254
"2",
255
"3",
256
"4",
257
"Exceeds Mastery",
258
"3",
259
"Meets Mastery",
260
"2",
261
"Near Mastery",
262
"1",
263
"Well Below Mastery",
264
"0",
265
"Insufficient Work to Assess",
266
]
267
268
def print_preview(self):
269
ex = self.generate_exercises(amount=1,regenerate=True,save=False)[0]
270
display(HTML("<h2>Preview:</h2> "+ex.html()))
271
ex.print_preview()
272
273
274
class Exercise:
275
def __init__(self, data=None, seed=None, outcome=None):
276
self.data = data
277
self.seed = seed
278
self.outcome = outcome
279
280
def data_tree(self):
281
return dict_to_tree(self.data,self.seed)
282
283
def pretext_tree(self):
284
transform = self.outcome.template()
285
tree = transform(self.data_tree()).getroot()
286
tree.xpath("/*")[0].attrib['checkit-seed'] = f"{self.seed:04}"
287
tree.xpath("/*")[0].attrib['checkit-slug'] = str(self.outcome.slug)
288
tree.xpath("/*")[0].attrib['checkit-title'] = str(self.outcome.title)
289
return tree
290
291
def pretext(self):
292
return str(lxml.etree.tostring(self.pretext_tree(), pretty_print=True), encoding="UTF-8")
293
294
def html_tree(self):
295
transform = TRANSFORM["html"]
296
tree = transform(self.pretext_tree()).getroot()
297
return tree
298
299
def html(self):
300
return str(lxml.etree.tostring(self.html_tree(),pretty_print=True), 'UTF-8')
301
302
def latex(self):
303
transform = TRANSFORM["latex"]
304
return str(transform(self.pretext_tree()))
305
306
def qti_tree(self):
307
transform = TRANSFORM["qti"]
308
tree = transform(self.pretext_tree()).getroot()
309
for mattextxml in tree.xpath("//mattextxml"):
310
mattext = lxml.etree.Element("mattext")
311
mattext.attrib['texttype'] = 'text/html'
312
mattext.text = lxml.html.tostring(lxml.html.fromstring(lxml.etree.tostring(mattextxml.find("*"),pretty_print=True)),pretty_print=True)
313
mattextxml.addnext(mattext)
314
return tree
315
316
def qti(self):
317
return str(lxml.etree.tostring(self.qti_tree(),pretty_print=True), 'UTF-8')
318
319
def dict(self):
320
return {
321
"seed": self.seed,
322
"qti": self.qti(),
323
"pretext": self.pretext(),
324
"html": self.html(),
325
"tex": self.latex(),
326
}
327
328
def print_preview(self):
329
print("Data XML")
330
print("-----------")
331
print(str(lxml.etree.tostring(self.data_tree(), pretty_print=True), "UTF-8"))
332
print()
333
print("HTML source")
334
print("-----------")
335
print(self.html())
336
print()
337
print("LaTeX source")
338
print("------------")
339
print(self.latex())
340
print()
341
print("QTI source")
342
print("------------")
343
print(self.qti())
344
print()
345
print("PreTeXt source")
346
print("------------")
347
print(self.pretext())
348
349
350
# def build_files(
351
# self,
352
# build_path="__build__",
353
# bank_title="CheckIt Question Bank"
354
# ):
355
# # provision filesystem
356
# if not os.path.isdir(build_path): os.mkdir(build_path)
357
# outcome_build_path = os.path.join(build_path, self.__slug)
358
# if not os.path.isdir(outcome_build_path): os.mkdir(outcome_build_path)
359
# qtibank_build_path = os.path.join(build_path, "qti-bank")
360
# if not os.path.isdir(qtibank_build_path): os.mkdir(qtibank_build_path)
361
# print(f"Building {outcome_build_path}...")
362
363
# qtibank_tree = self.qtibank_generic_tree(bank_title)
364
365
# for count,seed in enumerate(self.__seeds):
366
# exercise = self.list()[count]
367
# # build flat files
368
# with open(f'{outcome_build_path}/{count:04}.ptx','w') as outfile:
369
# print(exercise.pretext(), file=outfile)
370
# with open(f'{outcome_build_path}/{count:04}.tex','w') as outfile:
371
# print(exercise.latex(), file=outfile)
372
# with open(f'{outcome_build_path}/{count:04}.html','w') as outfile:
373
# print(exercise.html(), file=outfile)
374
# with open(f'{outcome_build_path}/{count:04}.qti','w') as outfile:
375
# print(exercise.qti(), file=outfile)
376
# # add to qtibank file
377
# qtibank_tree.find("*").append(exercise.qti_tree())
378
# qtibank_tree.find("*").attrib['ident'] = self.__slug
379
# with open(f'{qtibank_build_path}/{self.__slug}.qti','w') as outfile:
380
# print(str(lxml.etree.tostring(qtibank_tree, encoding="UTF-8", xml_declaration=True,pretty_print=True),"UTF-8"), file=outfile)
381
# print(f"- Files built successfully!")
382
383
384
385
# Bank building
386
def build_bank(bank_path, amount=50, fixed=False, public=False):
387
config = lxml.etree.parse(os.path.join(bank_path, "__bank__.xml"))
388
bank_title = config.find("title").text
389
bank_slug = config.find("slug").text
390
# build Canvas outcome CSV
391
outcome_csv = [[
392
"vendor_guid",
393
"object_type",
394
"title",
395
"description",
396
"display_name",
397
"calculation_method",
398
"calculation_int",
399
"mastery_points",
400
"ratings",
401
]]
402
# build JSON blob for bank
403
bank_json = {
404
"title": bank_title,
405
"slug": bank_slug,
406
"outcomes": [],
407
}
408
# Canvas chokes on repeated IDs from mult instructors in same institution
409
for n,objective in enumerate(config.xpath("objectives/objective")):
410
slug = objective.find("slug").text
411
title = objective.find("title").text
412
description = objective.find("description").text
413
alignment = objective.find("alignment").text
414
oldwd=os.getcwd();os.chdir(bank_path)
415
load(f"{slug}.sage") # imports `generator` function
416
os.chdir(oldwd)
417
with open(os.path.join(bank_path, f"{slug}.ptx"),'r') as template_file:
418
template = template_file.read()
419
outcome = Outcome(
420
title=title,
421
slug=slug,
422
description=description,
423
alignment=alignment,
424
generator=generator,
425
template=template,
426
amount=amount,
427
fixed=fixed,
428
public=public,
429
)
430
outcome.build_files(
431
build_path=os.path.join(bank_path,"__build__"),
432
bank_title=bank_title,
433
)
434
bank_json["outcomes"].append(outcome.dict())
435
outcome_csv.append(outcome.outcome_csv_row(n,bank_slug,oid_suffix))
436
print("Canvas outcomes built.")
437
import json
438
with open(os.path.join(bank_path, "__build__", f"{bank_slug}-bank.json"),'w') as f:
439
json.dump(bank_json,f)
440
print("JSON blob built.")
441
print("Bank build complete!")
442
443