QuizGenerator 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- QuizGenerator/README.md +5 -0
- QuizGenerator/__init__.py +27 -0
- QuizGenerator/__main__.py +7 -0
- QuizGenerator/canvas/__init__.py +13 -0
- QuizGenerator/canvas/canvas_interface.py +627 -0
- QuizGenerator/canvas/classes.py +235 -0
- QuizGenerator/constants.py +149 -0
- QuizGenerator/contentast.py +1955 -0
- QuizGenerator/generate.py +253 -0
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/misc.py +579 -0
- QuizGenerator/mixins.py +548 -0
- QuizGenerator/performance.py +202 -0
- QuizGenerator/premade_questions/__init__.py +0 -0
- QuizGenerator/premade_questions/basic.py +103 -0
- QuizGenerator/premade_questions/cst334/__init__.py +1 -0
- QuizGenerator/premade_questions/cst334/languages.py +391 -0
- QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
- QuizGenerator/premade_questions/cst334/memory_questions.py +1400 -0
- QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
- QuizGenerator/premade_questions/cst334/persistence_questions.py +451 -0
- QuizGenerator/premade_questions/cst334/process.py +648 -0
- QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
- QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
- QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/models/attention.py +192 -0
- QuizGenerator/premade_questions/cst463/models/cnns.py +186 -0
- QuizGenerator/premade_questions/cst463/models/matrices.py +24 -0
- QuizGenerator/premade_questions/cst463/models/rnns.py +202 -0
- QuizGenerator/premade_questions/cst463/models/text.py +203 -0
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +227 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1314 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
- QuizGenerator/qrcode_generator.py +293 -0
- QuizGenerator/question.py +715 -0
- QuizGenerator/quiz.py +467 -0
- QuizGenerator/regenerate.py +472 -0
- QuizGenerator/typst_utils.py +113 -0
- quizgenerator-0.4.2.dist-info/METADATA +265 -0
- quizgenerator-0.4.2.dist-info/RECORD +52 -0
- quizgenerator-0.4.2.dist-info/WHEEL +4 -0
- quizgenerator-0.4.2.dist-info/entry_points.txt +3 -0
- quizgenerator-0.4.2.dist-info/licenses/LICENSE +674 -0
QuizGenerator/mixins.py
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
#!env python
|
|
2
|
+
"""
|
|
3
|
+
Mixin classes to reduce boilerplate in question generation.
|
|
4
|
+
These mixins provide reusable patterns for common question structures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import abc
|
|
8
|
+
from typing import Dict, List, Any, Union
|
|
9
|
+
from QuizGenerator.misc import Answer
|
|
10
|
+
from QuizGenerator.contentast import ContentAST
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TableQuestionMixin:
|
|
14
|
+
"""
|
|
15
|
+
Mixin providing common table generation patterns for questions.
|
|
16
|
+
|
|
17
|
+
This mixin identifies and abstracts the most common table patterns used
|
|
18
|
+
across question types, reducing repetitive ContentAST.Table creation code.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def create_info_table(self, info_dict: Dict[str, Any], transpose: bool = False) -> ContentAST.Table:
|
|
22
|
+
"""
|
|
23
|
+
Creates a vertical info table (key-value pairs).
|
|
24
|
+
|
|
25
|
+
Common pattern: Display parameters/givens in a clean table format.
|
|
26
|
+
Used by: HardDriveAccessTime, BaseAndBounds, etc.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
info_dict: Dictionary of {label: value} pairs
|
|
30
|
+
transpose: Whether to transpose the table (default: False)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
ContentAST.Table with the information formatted
|
|
34
|
+
"""
|
|
35
|
+
# Don't convert ContentAST elements to strings - let them render properly
|
|
36
|
+
table_data = []
|
|
37
|
+
for key, value in info_dict.items():
|
|
38
|
+
# Keep ContentAST elements as-is, convert others to strings
|
|
39
|
+
if isinstance(value, ContentAST.Element):
|
|
40
|
+
table_data.append([key, value])
|
|
41
|
+
else:
|
|
42
|
+
table_data.append([key, str(value)])
|
|
43
|
+
|
|
44
|
+
return ContentAST.Table(
|
|
45
|
+
data=table_data,
|
|
46
|
+
transpose=transpose
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def create_answer_table(
|
|
50
|
+
self,
|
|
51
|
+
headers: List[str],
|
|
52
|
+
data_rows: List[Dict[str, Any]],
|
|
53
|
+
answer_columns: List[str] = None
|
|
54
|
+
) -> ContentAST.Table:
|
|
55
|
+
"""
|
|
56
|
+
Creates a table where some cells are answer blanks.
|
|
57
|
+
|
|
58
|
+
Common pattern: Mix of given data and answer blanks in a structured table.
|
|
59
|
+
Used by: VirtualAddressParts, SchedulingQuestion, CachingQuestion, etc.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
headers: Column headers for the table
|
|
63
|
+
data_rows: List of dictionaries, each representing a row
|
|
64
|
+
answer_columns: List of column names that should be treated as answers
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
ContentAST.Table with answers embedded in appropriate cells
|
|
68
|
+
"""
|
|
69
|
+
answer_columns = answer_columns or []
|
|
70
|
+
|
|
71
|
+
def format_cell(row_data: Dict, column: str) -> Union[str, ContentAST.Answer]:
|
|
72
|
+
"""Format a cell based on whether it should be an answer or plain data"""
|
|
73
|
+
value = row_data.get(column, "")
|
|
74
|
+
|
|
75
|
+
# If this column should contain answers and the value is an Answer object
|
|
76
|
+
if column in answer_columns and isinstance(value, Answer):
|
|
77
|
+
return ContentAST.Answer(value)
|
|
78
|
+
# If this column should contain answers but we have the answer key
|
|
79
|
+
elif column in answer_columns and isinstance(value, str) and hasattr(self, 'answers'):
|
|
80
|
+
answer_obj = self.answers.get(value)
|
|
81
|
+
if answer_obj:
|
|
82
|
+
return ContentAST.Answer(answer_obj)
|
|
83
|
+
|
|
84
|
+
# Otherwise return as plain data
|
|
85
|
+
return str(value)
|
|
86
|
+
|
|
87
|
+
table_data = [
|
|
88
|
+
[format_cell(row, header) for header in headers]
|
|
89
|
+
for row in data_rows
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
return ContentAST.Table(
|
|
93
|
+
headers=headers,
|
|
94
|
+
data=table_data
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def create_parameter_answer_table(
|
|
98
|
+
self,
|
|
99
|
+
parameter_info: Dict[str, Any],
|
|
100
|
+
answer_label: str,
|
|
101
|
+
answer_key: str,
|
|
102
|
+
transpose: bool = True
|
|
103
|
+
) -> ContentAST.Table:
|
|
104
|
+
"""
|
|
105
|
+
Creates a table combining parameters with a single answer.
|
|
106
|
+
|
|
107
|
+
Common pattern: Show parameters/context, then ask for one calculated result.
|
|
108
|
+
Used by: BaseAndBounds, many memory questions, etc.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
parameter_info: Dictionary of {parameter_name: value}
|
|
112
|
+
answer_label: Label for the answer row
|
|
113
|
+
answer_key: Key to look up the answer in self.answers
|
|
114
|
+
transpose: Whether to show as vertical table (default: True)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
ContentAST.Table with parameters and answer
|
|
118
|
+
"""
|
|
119
|
+
# Build data with parameters plus answer row
|
|
120
|
+
data = [[key, str(value)] for key, value in parameter_info.items()]
|
|
121
|
+
|
|
122
|
+
# Add answer row
|
|
123
|
+
if hasattr(self, 'answers') and answer_key in self.answers:
|
|
124
|
+
data.append([answer_label, ContentAST.Answer(self.answers[answer_key])])
|
|
125
|
+
else:
|
|
126
|
+
data.append([answer_label, f"[{answer_key}]"]) # Fallback
|
|
127
|
+
|
|
128
|
+
return ContentAST.Table(
|
|
129
|
+
data=data,
|
|
130
|
+
transpose=transpose
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def create_fill_in_table(
|
|
134
|
+
self,
|
|
135
|
+
headers: List[str],
|
|
136
|
+
template_rows: List[Dict[str, Any]]
|
|
137
|
+
) -> ContentAST.Table:
|
|
138
|
+
"""
|
|
139
|
+
Creates a table where multiple cells are answer blanks to fill in.
|
|
140
|
+
|
|
141
|
+
Common pattern: Show a partially completed table where students fill blanks.
|
|
142
|
+
Used by: CachingQuestion, SchedulingQuestion, etc.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
headers: Column headers
|
|
146
|
+
template_rows: Rows where values can be data or answer keys
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
ContentAST.Table with multiple answer blanks
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def process_cell_value(value: Any) -> Union[str, ContentAST.Answer]:
|
|
153
|
+
"""Convert cell values to appropriate display format"""
|
|
154
|
+
# If it's already an Answer object, wrap it
|
|
155
|
+
if isinstance(value, Answer):
|
|
156
|
+
return ContentAST.Answer(value)
|
|
157
|
+
# If it's a string that looks like an answer key, try to resolve it
|
|
158
|
+
elif isinstance(value, str) and value.startswith("answer__") and hasattr(self, 'answers'):
|
|
159
|
+
answer_obj = self.answers.get(value)
|
|
160
|
+
if answer_obj:
|
|
161
|
+
return ContentAST.Answer(answer_obj)
|
|
162
|
+
# Otherwise return as-is
|
|
163
|
+
return str(value)
|
|
164
|
+
|
|
165
|
+
table_data = [
|
|
166
|
+
[process_cell_value(row.get(header, "")) for header in headers]
|
|
167
|
+
for row in template_rows
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
return ContentAST.Table(
|
|
171
|
+
headers=headers,
|
|
172
|
+
data=table_data
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class BodyTemplatesMixin:
|
|
177
|
+
"""
|
|
178
|
+
Mixin providing common body structure patterns.
|
|
179
|
+
|
|
180
|
+
These methods create complete ContentAST.Section objects following
|
|
181
|
+
common question layout patterns.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def create_calculation_with_info_body(
|
|
185
|
+
self,
|
|
186
|
+
intro_text: str,
|
|
187
|
+
info_table: ContentAST.Table,
|
|
188
|
+
answer_block: ContentAST.AnswerBlock
|
|
189
|
+
) -> ContentAST.Section:
|
|
190
|
+
"""
|
|
191
|
+
Standard pattern: intro text + info table + answer block.
|
|
192
|
+
|
|
193
|
+
Used by: HardDriveAccessTime, AverageMemoryAccessTime, etc.
|
|
194
|
+
"""
|
|
195
|
+
body = ContentAST.Section()
|
|
196
|
+
body.add_element(ContentAST.Paragraph([intro_text]))
|
|
197
|
+
body.add_element(info_table)
|
|
198
|
+
body.add_element(answer_block)
|
|
199
|
+
return body
|
|
200
|
+
|
|
201
|
+
def create_fill_in_table_body(
|
|
202
|
+
self,
|
|
203
|
+
intro_text: str,
|
|
204
|
+
instructions: str,
|
|
205
|
+
table: ContentAST.Table
|
|
206
|
+
) -> ContentAST.Section:
|
|
207
|
+
"""
|
|
208
|
+
Standard pattern: intro + instructions + table with blanks.
|
|
209
|
+
|
|
210
|
+
Used by: VirtualAddressParts, CachingQuestion, etc.
|
|
211
|
+
"""
|
|
212
|
+
body = ContentAST.Section()
|
|
213
|
+
if intro_text:
|
|
214
|
+
body.add_element(ContentAST.Paragraph([intro_text]))
|
|
215
|
+
if instructions:
|
|
216
|
+
body.add_element(ContentAST.Paragraph([instructions]))
|
|
217
|
+
body.add_element(table)
|
|
218
|
+
return body
|
|
219
|
+
|
|
220
|
+
def create_parameter_calculation_body(
|
|
221
|
+
self,
|
|
222
|
+
intro_text: str,
|
|
223
|
+
parameter_table: ContentAST.Table,
|
|
224
|
+
answer_table: ContentAST.Table = None,
|
|
225
|
+
additional_instructions: str = None
|
|
226
|
+
) -> ContentAST.Section:
|
|
227
|
+
"""
|
|
228
|
+
Standard pattern: intro + parameter table + optional answer table.
|
|
229
|
+
|
|
230
|
+
Used by: BaseAndBounds, Paging, etc.
|
|
231
|
+
"""
|
|
232
|
+
body = ContentAST.Section()
|
|
233
|
+
body.add_element(ContentAST.Paragraph([intro_text]))
|
|
234
|
+
body.add_element(parameter_table)
|
|
235
|
+
|
|
236
|
+
if additional_instructions:
|
|
237
|
+
body.add_element(ContentAST.Paragraph([additional_instructions]))
|
|
238
|
+
|
|
239
|
+
if answer_table:
|
|
240
|
+
body.add_element(answer_table)
|
|
241
|
+
|
|
242
|
+
return body
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class MultiPartQuestionMixin:
|
|
246
|
+
"""
|
|
247
|
+
Mixin providing multi-part question generation with labeled subparts (a), (b), (c), etc.
|
|
248
|
+
|
|
249
|
+
This mixin enables questions to be split into multiple subparts when num_subquestions > 1.
|
|
250
|
+
Each subpart gets its own calculation with proper (a), (b), (c) labeling and alignment.
|
|
251
|
+
Primarily designed for vector math questions but extensible to other question types.
|
|
252
|
+
|
|
253
|
+
Usage:
|
|
254
|
+
class VectorDotProduct(VectorMathQuestion, MultiPartQuestionMixin):
|
|
255
|
+
def get_body(self):
|
|
256
|
+
if self.is_multipart():
|
|
257
|
+
return self.create_multipart_body()
|
|
258
|
+
else:
|
|
259
|
+
return self.create_single_part_body()
|
|
260
|
+
|
|
261
|
+
Methods provided:
|
|
262
|
+
- is_multipart(): Check if this question should generate multiple subparts
|
|
263
|
+
- create_repeated_problem_part(): Create the ContentAST.RepeatedProblemPart element
|
|
264
|
+
- generate_subquestion_data(): Abstract method for subclasses to implement
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
def is_multipart(self):
|
|
268
|
+
"""
|
|
269
|
+
Check if this question should generate multiple subparts.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
bool: True if num_subquestions > 1, False otherwise
|
|
273
|
+
"""
|
|
274
|
+
return getattr(self, 'num_subquestions', 1) > 1
|
|
275
|
+
|
|
276
|
+
def create_repeated_problem_part(self, subpart_data_list):
|
|
277
|
+
"""
|
|
278
|
+
Create a ContentAST.RepeatedProblemPart element from subpart data.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
subpart_data_list: List of data for each subpart. Each item can be:
|
|
282
|
+
- A string (LaTeX equation content)
|
|
283
|
+
- A ContentAST.Element
|
|
284
|
+
- A tuple/list of elements to be joined
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
ContentAST.RepeatedProblemPart: The formatted multi-part element
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
# For vector dot products
|
|
291
|
+
subparts = [
|
|
292
|
+
(matrix_a1, "\\cdot", matrix_b1),
|
|
293
|
+
(matrix_a2, "\\cdot", matrix_b2)
|
|
294
|
+
]
|
|
295
|
+
return self.create_repeated_problem_part(subparts)
|
|
296
|
+
"""
|
|
297
|
+
from QuizGenerator.contentast import ContentAST
|
|
298
|
+
return ContentAST.RepeatedProblemPart(subpart_data_list)
|
|
299
|
+
|
|
300
|
+
def generate_subquestion_data(self):
|
|
301
|
+
"""
|
|
302
|
+
Generate data for each subpart of the question.
|
|
303
|
+
|
|
304
|
+
This is an abstract method that subclasses must implement.
|
|
305
|
+
It should generate and return the data needed for each subpart.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
list: List of data for each subpart. The format depends on the
|
|
309
|
+
specific question type but should be compatible with
|
|
310
|
+
ContentAST.RepeatedProblemPart.
|
|
311
|
+
|
|
312
|
+
Example implementation:
|
|
313
|
+
def generate_subquestion_data(self):
|
|
314
|
+
subparts = []
|
|
315
|
+
for i in range(self.num_subquestions):
|
|
316
|
+
vector_a = self._generate_vector(self.dimension)
|
|
317
|
+
vector_b = self._generate_vector(self.dimension)
|
|
318
|
+
matrix_a = ContentAST.Matrix.to_latex(
|
|
319
|
+
[[v] for v in vector_a], "b"
|
|
320
|
+
)
|
|
321
|
+
matrix_b = ContentAST.Matrix.to_latex(
|
|
322
|
+
[[v] for v in vector_b], "b"
|
|
323
|
+
)
|
|
324
|
+
subparts.append((matrix_a, "\\cdot", matrix_b))
|
|
325
|
+
return subparts
|
|
326
|
+
"""
|
|
327
|
+
raise NotImplementedError(
|
|
328
|
+
"Subclasses using MultiPartQuestionMixin must implement generate_subquestion_data()"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def create_multipart_body(self, intro_text="Calculate the following:"):
|
|
332
|
+
"""
|
|
333
|
+
Create a standard multipart question body using the repeated problem part format.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
intro_text: Introduction text for the question
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
ContentAST.Section: Complete question body with intro and subparts
|
|
340
|
+
|
|
341
|
+
Example:
|
|
342
|
+
def get_body(self):
|
|
343
|
+
if self.is_multipart():
|
|
344
|
+
return self.create_multipart_body("Calculate the dot products:")
|
|
345
|
+
else:
|
|
346
|
+
return self.create_single_part_body()
|
|
347
|
+
"""
|
|
348
|
+
from QuizGenerator.contentast import ContentAST
|
|
349
|
+
body = ContentAST.Section()
|
|
350
|
+
body.add_element(ContentAST.Paragraph([intro_text]))
|
|
351
|
+
|
|
352
|
+
# Generate subpart data and create the repeated problem part
|
|
353
|
+
subpart_data = self.generate_subquestion_data()
|
|
354
|
+
repeated_part = self.create_repeated_problem_part(subpart_data)
|
|
355
|
+
body.add_element(repeated_part)
|
|
356
|
+
|
|
357
|
+
return body
|
|
358
|
+
|
|
359
|
+
def get_subpart_answers(self):
|
|
360
|
+
"""
|
|
361
|
+
Retrieve answers organized by subpart for multipart questions.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
dict: Dictionary mapping subpart letters ('a', 'b', 'c') to their answers.
|
|
365
|
+
Returns empty dict if not a multipart question.
|
|
366
|
+
|
|
367
|
+
Example:
|
|
368
|
+
# For a 3-part question
|
|
369
|
+
{
|
|
370
|
+
'a': Answer.integer('a', 5),
|
|
371
|
+
'b': Answer.integer('b', 12),
|
|
372
|
+
'c': Answer.integer('c', -3)
|
|
373
|
+
}
|
|
374
|
+
"""
|
|
375
|
+
if not self.is_multipart():
|
|
376
|
+
return {}
|
|
377
|
+
|
|
378
|
+
subpart_answers = {}
|
|
379
|
+
for i in range(self.num_subquestions):
|
|
380
|
+
letter = chr(ord('a') + i)
|
|
381
|
+
# Look for answers with subpart keys
|
|
382
|
+
answer_key = f"subpart_{letter}"
|
|
383
|
+
if hasattr(self, 'answers') and answer_key in self.answers:
|
|
384
|
+
subpart_answers[letter] = self.answers[answer_key]
|
|
385
|
+
|
|
386
|
+
return subpart_answers
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
|
|
390
|
+
"""
|
|
391
|
+
Abstract base class for mathematical operation questions (vectors, matrices, etc.).
|
|
392
|
+
|
|
393
|
+
This class provides common infrastructure for questions that:
|
|
394
|
+
- Perform operations on mathematical objects (vectors, matrices)
|
|
395
|
+
- Support both single and multipart questions
|
|
396
|
+
- Use LaTeX formatting for equations
|
|
397
|
+
- Generate step-by-step explanations
|
|
398
|
+
|
|
399
|
+
Subclasses must implement abstract methods for:
|
|
400
|
+
- Generating operands (vectors, matrices, etc.)
|
|
401
|
+
- Performing the mathematical operation
|
|
402
|
+
- Formatting results for LaTeX display
|
|
403
|
+
- Creating answer objects
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
def __init__(self, *args, **kwargs):
|
|
407
|
+
kwargs["topic"] = kwargs.get("topic", "MATH") # Default to MATH topic
|
|
408
|
+
super().__init__(*args, **kwargs)
|
|
409
|
+
|
|
410
|
+
# Abstract methods that subclasses must implement
|
|
411
|
+
|
|
412
|
+
@abc.abstractmethod
|
|
413
|
+
def get_operator(self):
|
|
414
|
+
"""Return the LaTeX operator for this operation (e.g., '+', '\\cdot', '\\times')."""
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
@abc.abstractmethod
|
|
418
|
+
def calculate_single_result(self, operand_a, operand_b):
|
|
419
|
+
"""Calculate the result for a single question with two operands."""
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
@abc.abstractmethod
|
|
423
|
+
def create_subquestion_answers(self, subpart_index, result):
|
|
424
|
+
"""Create answer objects for a subquestion result."""
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
@abc.abstractmethod
|
|
428
|
+
def generate_operands(self):
|
|
429
|
+
"""Generate two operands for the operation. Returns (operand_a, operand_b)."""
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
@abc.abstractmethod
|
|
433
|
+
def format_operand_latex(self, operand):
|
|
434
|
+
"""Format an operand for LaTeX display."""
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
@abc.abstractmethod
|
|
438
|
+
def format_single_equation(self, operand_a, operand_b):
|
|
439
|
+
"""Format the equation for single questions. Returns LaTeX string."""
|
|
440
|
+
pass
|
|
441
|
+
|
|
442
|
+
# Common implementation methods
|
|
443
|
+
|
|
444
|
+
def get_intro_text(self):
|
|
445
|
+
"""Default intro text - subclasses can override."""
|
|
446
|
+
return "Calculate the following:"
|
|
447
|
+
|
|
448
|
+
def create_single_answers(self, result):
|
|
449
|
+
"""Create answers for single questions - just delegate to subquestion method."""
|
|
450
|
+
return self.create_subquestion_answers(0, result)
|
|
451
|
+
|
|
452
|
+
def refresh(self, *args, **kwargs):
|
|
453
|
+
super().refresh(*args, **kwargs)
|
|
454
|
+
|
|
455
|
+
# Clear any existing data
|
|
456
|
+
self.answers = {}
|
|
457
|
+
|
|
458
|
+
if self.is_multipart():
|
|
459
|
+
# Generate multiple subquestions
|
|
460
|
+
self.subquestion_data = []
|
|
461
|
+
for i in range(self.num_subquestions):
|
|
462
|
+
# Generate unique operands for each subquestion
|
|
463
|
+
operand_a, operand_b = self.generate_operands()
|
|
464
|
+
result = self.calculate_single_result(operand_a, operand_b)
|
|
465
|
+
|
|
466
|
+
self.subquestion_data.append(
|
|
467
|
+
{
|
|
468
|
+
'operand_a': operand_a,
|
|
469
|
+
'operand_b': operand_b,
|
|
470
|
+
'vector_a': operand_a, # For vector compatibility
|
|
471
|
+
'vector_b': operand_b, # For vector compatibility
|
|
472
|
+
'result': result
|
|
473
|
+
}
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Create answers for this subpart
|
|
477
|
+
self.create_subquestion_answers(i, result)
|
|
478
|
+
else:
|
|
479
|
+
# Single question (original behavior)
|
|
480
|
+
self.operand_a, self.operand_b = self.generate_operands()
|
|
481
|
+
self.result = self.calculate_single_result(self.operand_a, self.operand_b)
|
|
482
|
+
|
|
483
|
+
# Create answers
|
|
484
|
+
self.create_single_answers(self.result)
|
|
485
|
+
|
|
486
|
+
def generate_subquestion_data(self):
|
|
487
|
+
"""Generate LaTeX content for each subpart of the question."""
|
|
488
|
+
subparts = []
|
|
489
|
+
for data in self.subquestion_data:
|
|
490
|
+
operand_a_latex = self.format_operand_latex(data['operand_a'])
|
|
491
|
+
operand_b_latex = self.format_operand_latex(data['operand_b'])
|
|
492
|
+
# Return as tuple of (operand_a, operator, operand_b)
|
|
493
|
+
subparts.append((operand_a_latex, self.get_operator(), operand_b_latex))
|
|
494
|
+
return subparts
|
|
495
|
+
|
|
496
|
+
def get_body(self):
|
|
497
|
+
body = ContentAST.Section()
|
|
498
|
+
|
|
499
|
+
body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
|
|
500
|
+
|
|
501
|
+
if self.is_multipart():
|
|
502
|
+
# Use multipart formatting with repeated problem parts
|
|
503
|
+
subpart_data = self.generate_subquestion_data()
|
|
504
|
+
repeated_part = self.create_repeated_problem_part(subpart_data)
|
|
505
|
+
body.add_element(repeated_part)
|
|
506
|
+
else:
|
|
507
|
+
# Single equation display
|
|
508
|
+
equation_latex = self.format_single_equation(self.operand_a, self.operand_b)
|
|
509
|
+
body.add_element(ContentAST.Equation(f"{equation_latex} = ", inline=False))
|
|
510
|
+
|
|
511
|
+
# Canvas-only answer fields (hidden from PDF)
|
|
512
|
+
self._add_single_question_answers(body)
|
|
513
|
+
|
|
514
|
+
return body
|
|
515
|
+
|
|
516
|
+
def _add_single_question_answers(self, body):
|
|
517
|
+
"""Add Canvas-only answer fields for single questions. Subclasses can override."""
|
|
518
|
+
# Default implementation - subclasses should override for specific answer formats
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
def get_explanation(self):
|
|
522
|
+
"""Default explanation structure. Subclasses should override for specific explanations."""
|
|
523
|
+
explanation = ContentAST.Section()
|
|
524
|
+
|
|
525
|
+
explanation.add_element(ContentAST.Paragraph([self.get_explanation_intro()]))
|
|
526
|
+
|
|
527
|
+
if self.is_multipart():
|
|
528
|
+
# Handle multipart explanations
|
|
529
|
+
for i, data in enumerate(self.subquestion_data):
|
|
530
|
+
letter = chr(ord('a') + i)
|
|
531
|
+
explanation.add_element(self.create_explanation_for_subpart(data, letter))
|
|
532
|
+
else:
|
|
533
|
+
# Single part explanation
|
|
534
|
+
explanation.add_element(self.create_single_explanation())
|
|
535
|
+
|
|
536
|
+
return explanation
|
|
537
|
+
|
|
538
|
+
def get_explanation_intro(self):
|
|
539
|
+
"""Get the intro text for explanations. Subclasses should override."""
|
|
540
|
+
return "The calculation is performed as follows:"
|
|
541
|
+
|
|
542
|
+
def create_explanation_for_subpart(self, subpart_data, letter):
|
|
543
|
+
"""Create explanation for a single subpart. Subclasses should override."""
|
|
544
|
+
return ContentAST.Paragraph([f"Part ({letter}): Calculation details would go here."])
|
|
545
|
+
|
|
546
|
+
def create_single_explanation(self):
|
|
547
|
+
"""Create explanation for single questions. Subclasses should override."""
|
|
548
|
+
return ContentAST.Paragraph(["Single question explanation would go here."])
|