Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 16799
1
from lxml import etree
2
import lxml.html
3
4
def insert_object_into_element(obj,name,element):
5
if obj is False:
6
return None #skip generating element only when exactly False (not falsy)
7
se = etree.SubElement(element, name)
8
if isinstance(obj, list):
9
for item in obj:
10
insert_object_into_element(item,"item",se)
11
elif isinstance(obj, dict):
12
for key in obj.keys():
13
insert_object_into_element(obj[key],key,se)
14
else:
15
if isinstance(obj,str):
16
se.text = obj
17
else:
18
se.text = f" {latex(obj)} "
19
20
def dict_to_tree(data_dict):
21
tree = etree.Element("data")
22
for key in data_dict.keys():
23
insert_object_into_element(data_dict[key], key, tree)
24
return tree
25
26
27
28
import os
29
# always change working directory to the directory containing main.sage:
30
# oldwd=os.getcwd();os.chdir("path/to/dir");load("main.sage");os.chdir(oldwd)
31
SCRIPT_DIR = os.getcwd()
32
33
34
35
def mi_vars(*latex_names, random_order=True):
36
"""
37
Given one or more `latex_names` of strings, returns a tuple
38
of Sage variables. `random_order` names them so that they appear
39
in expressions in a random order.
40
"""
41
stamp = randrange(100000,999999)
42
indices = list(range(len(latex_names)))
43
if random_order:
44
shuffle(indices)
45
import string
46
random_letter = choice(list(string.ascii_lowercase))
47
return (var(f"{random_letter}_mi_var_{stamp}_{indices[i]}", latex_name=name) for i, name in enumerate(latex_names))
48
49
def shuffled_equation(*terms):
50
"""
51
Represents the equation sum(terms)==0, but with terms shuffled randomly
52
to each side.
53
"""
54
new_equation = (SR(0)==0)
55
for term in terms:
56
if choice([True,False]):
57
new_equation += (SR(term)==0)
58
else:
59
new_equation += (0==-SR(term))
60
return new_equation*choice([-1,1])
61
62
def base64_graphic(obj, file_format="svg"):
63
"""
64
Generates Base64 encoding of the graphic in the requested file_format.
65
"""
66
if not isinstance(obj,Graphics):
67
raise TypeError("Only graphics may be encoded as base64")
68
if file_format not in ["svg", "png"]:
69
raise ValueError("Invalid file format")
70
filename = tmp_filename(ext=f'.{file_format}')
71
obj.save(filename)
72
with open(filename, 'rb') as f:
73
from base64 import b64encode
74
b64 = b64encode(f.read())
75
return b64
76
77
def data_url_graphic(obj, file_format="svg"):
78
"""
79
Generates Data URL representing the graphic in the requested file_format.
80
"""
81
b64 = base64_graphic(obj, file_format=file_format).decode('utf-8')
82
if file_format=="svg":
83
file_format = "svg+xml"
84
return f"data:image/{file_format};base64,{b64}"
85
86
def latex_system_from_matrix(matrix, variables="x", alpha_mode=False, variable_list=[]):
87
# Augment with zero vector if not already augmented
88
if not matrix.subdivisions()[1]:
89
matrix=matrix.augment(zero_vector(ZZ, len(matrix.rows())), subdivide=true)
90
num_vars = matrix.subdivisions()[1][0]
91
# Start using requested variables
92
system_vars = variable_list
93
# Conveniently add xyzwv if requested
94
if alpha_mode:
95
system_vars += list(var("x y z w v"))
96
# Finally fall back to x_n as needed
97
system_vars += [var(f"{variables}_{n+1}") for n in range(num_vars)]
98
# Build matrix
99
latex_output = "\\begin{matrix}\n"
100
for row in matrix.rows():
101
if row[0]!= 0:
102
latex_output += latex(row[0]*system_vars[0])
103
previous_terms = True
104
else:
105
previous_terms = False
106
for n,cell in enumerate(row[1:num_vars]):
107
latex_output += " & "
108
if cell < 0 and previous_terms:
109
latex_output += " - "
110
elif cell > 0 and previous_terms:
111
latex_output += " + "
112
latex_output += " & "
113
if cell != 0:
114
latex_output += latex(cell.abs()*system_vars[n+1])
115
if not previous_terms:
116
previous_terms = bool(cell!=0)
117
if not previous_terms:
118
latex_output += " 0 "
119
latex_output += " & = & "
120
latex_output += latex(row[num_vars])
121
latex_output += "\\\\\n"
122
latex_output += "\\end{matrix}"
123
return latex_output
124
125
126
class Exercise:
127
def __init__(self, name=None, slug=None, generator=None, template=None, seed=None):
128
self.__name = name
129
self.__slug = slug
130
self.__generator = generator
131
self.__template = template
132
self.reset_seed(seed=seed)
133
134
def reset_seed(self, seed=None):
135
if seed is None:
136
set_random_seed()
137
seed = randrange(0,10000)
138
self.__seed = seed
139
140
def data_dict(self):
141
set_random_seed(self.__seed)
142
return self.__generator()
143
144
def data_tree(self):
145
return dict_to_tree(self.data_dict())
146
147
def template(self):
148
PREFIX = """<?xml version="1.0"?>
149
<xsl:stylesheet version="1.0"
150
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
151
<xsl:output method="xml"/>
152
<xsl:template match="/data">
153
"""
154
SUFFIX = """
155
</xsl:template>
156
</xsl:stylesheet>
157
"""
158
return etree.XSLT(etree.XML(PREFIX+self.__template+SUFFIX))
159
160
def pretext_tree(self):
161
transform = self.template()
162
tree = transform(self.data_tree()).getroot()
163
tree.xpath("/*")[0].attrib['masterit-seed'] = f"{self.__seed:04}"
164
tree.xpath("/*")[0].attrib['masterit-slug'] = str(self.__slug)
165
tree.xpath("/*")[0].attrib['masterit-name'] = str(self.__name)
166
return tree
167
168
def pretext(self):
169
return str(etree.tostring(self.pretext_tree(), pretty_print=True), encoding="UTF-8")
170
171
def html_tree(self):
172
transform = etree.XSLT(etree.parse(os.path.join(SCRIPT_DIR,"xsl","html.xsl")))
173
tree = transform(self.pretext_tree()).getroot()
174
return tree
175
176
def html(self):
177
return str(etree.tostring(self.html_tree(),pretty_print=True), 'UTF-8')
178
179
def latex(self):
180
transform = etree.XSLT(etree.parse(os.path.join(SCRIPT_DIR,"xsl","latex.xsl")))
181
return str(transform(self.pretext_tree()))
182
183
def qti_tree(self):
184
transform = etree.XSLT(etree.parse(os.path.join(SCRIPT_DIR,"xsl","qti.xsl")))
185
tree = transform(self.pretext_tree()).getroot()
186
for mattextxml in tree.xpath("//mattextxml"):
187
mattext = etree.Element("mattext")
188
mattext.attrib['texttype'] = 'text/html'
189
mattext.text = lxml.html.tostring(lxml.html.fromstring(etree.tostring(mattextxml.find("*"),pretty_print=True)),pretty_print=True)
190
mattextxml.addnext(mattext)
191
return tree
192
193
def qti(self):
194
return str(etree.tostring(self.qti_tree(),pretty_print=True), 'UTF-8')
195
196
def preview(self):
197
print("Data XML")
198
print("-----------")
199
print(str(etree.tostring(self.data_tree(), pretty_print=True), "UTF-8"))
200
print()
201
print("HTML source")
202
print("-----------")
203
print(self.html())
204
print()
205
print("LaTeX source")
206
print("------------")
207
print(self.latex())
208
print()
209
print("QTI source")
210
print("------------")
211
print(self.qti())
212
print()
213
print("PreTeXt source")
214
print("------------")
215
print(self.pretext())
216
217
def build_files(self, amount=50, fixed=False, build_path="build", library_title="MasterIt Question Bank"):
218
if not os.path.isdir(build_path): os.mkdir(build_path)
219
obj_build_path = os.path.join(build_path, self.__slug)
220
if not os.path.isdir(obj_build_path): os.mkdir(obj_build_path)
221
bank_build_path = os.path.join(build_path, "qti-bank")
222
if not os.path.isdir(bank_build_path): os.mkdir(bank_build_path)
223
bank_tree = etree.fromstring("""<?xml version="1.0"?>
224
<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">
225
<objectbank>
226
<qtimetadata>
227
<qtimetadatafield/>
228
</qtimetadata>
229
</objectbank>
230
</questestinterop>""")
231
label = etree.SubElement(bank_tree.find("*/*/*"), "fieldlabel")
232
label.text = "bank_title"
233
entry = etree.SubElement(bank_tree.find("*/*/*"), "fieldentry")
234
entry.text = f"{library_title} -- {self.__slug}"
235
for count in range(0,amount):
236
if fixed:
237
self.reset_seed(count)
238
else:
239
self.reset_seed()
240
# build flat files
241
with open(f'{obj_build_path}/{count:04}.ptx','w') as outfile:
242
print(self.pretext(), file=outfile)
243
with open(f'{obj_build_path}/{count:04}.tex','w') as outfile:
244
print(self.latex(), file=outfile)
245
with open(f'{obj_build_path}/{count:04}.html','w') as outfile:
246
print(self.html(), file=outfile)
247
with open(f'{obj_build_path}/{count:04}.qti','w') as outfile:
248
print(self.qti(), file=outfile)
249
# add to bank file
250
bank_tree.find("*").append(self.qti_tree())
251
bank_tree.find("*").attrib['ident'] = self.__slug
252
with open(f'{bank_build_path}/{self.__slug}.qti','w') as outfile:
253
print(str(etree.tostring(bank_tree, encoding="UTF-8", xml_declaration=True,pretty_print=True),"UTF-8"), file=outfile)
254
print(f"Files built successfully at {obj_build_path}")
255
256
257
258
def build_library(library_path, amount=50, fixed=False):
259
config = etree.parse(os.path.join(library_path, "__bank__.xml"))
260
library_title = config.find("title").text
261
library_slug = config.find("slug").text
262
outcome_csv = [[
263
"vendor_guid",
264
"object_type",
265
"title",
266
"description",
267
"display_name",
268
"calculation_method",
269
"calculation_int",
270
"mastery_points",
271
"ratings",
272
]]
273
for n,objective in enumerate(config.xpath("objectives/objective")):
274
slug = objective.find("slug").text
275
title = objective.find("title").text
276
oldwd=os.getcwd();os.chdir(library_path)
277
load(f"{slug}.sage") # imports `generator` function
278
os.chdir(oldwd)
279
with open(os.path.join(library_path, f"{slug}.ptx"),'r') as template_file:
280
template = template_file.read()
281
Exercise(
282
name=objective.find("title").text,
283
slug=slug,
284
generator=generator,
285
template=template
286
).build_files(
287
library_title=library_title,
288
build_path=os.path.join(library_path,"build"),
289
amount=amount,
290
fixed=fixed,
291
)
292
outcome_csv.append([
293
f"{library_slug}_{n:02}_{slug}",
294
"outcome",
295
f"{n:02}-{slug}: {title}",
296
"",
297
slug,
298
"n_mastery",
299
"2",
300
"3",
301
"4",
302
"Exceeds Mastery",
303
"3",
304
"Meets Mastery",
305
"2",
306
"Near Mastery",
307
"1",
308
"Well Below Mastery",
309
"0",
310
"Insufficient Work to Assess",
311
])
312
import csv
313
with open(os.path.join(library_path, "build", "canvas-outcomes.csv"),'w') as f:
314
csv.writer(f).writerows(outcome_csv)
315
print("Canvas outcomes built.")
316
print("Library build complete!")
317
318