QuizGenerator 0.4.2__py3-none-any.whl → 0.6.0__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.
Files changed (33) hide show
  1. QuizGenerator/contentast.py +809 -117
  2. QuizGenerator/generate.py +219 -11
  3. QuizGenerator/misc.py +0 -556
  4. QuizGenerator/mixins.py +50 -29
  5. QuizGenerator/premade_questions/basic.py +3 -3
  6. QuizGenerator/premade_questions/cst334/languages.py +183 -175
  7. QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
  8. QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
  9. QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
  10. QuizGenerator/premade_questions/cst334/process.py +558 -79
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
  12. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
  13. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
  14. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
  15. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
  16. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
  17. QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
  18. QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
  19. QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
  20. QuizGenerator/premade_questions/cst463/models/text.py +29 -15
  21. QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
  22. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
  23. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
  24. QuizGenerator/question.py +114 -20
  25. QuizGenerator/quiz.py +81 -24
  26. QuizGenerator/regenerate.py +98 -29
  27. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
  28. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
  29. QuizGenerator/README.md +0 -5
  30. QuizGenerator/logging.yaml +0 -55
  31. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
  32. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
  33. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/mixins.py CHANGED
@@ -6,7 +6,6 @@ These mixins provide reusable patterns for common question structures.
6
6
 
7
7
  import abc
8
8
  from typing import Dict, List, Any, Union
9
- from QuizGenerator.misc import Answer
10
9
  from QuizGenerator.contentast import ContentAST
11
10
 
12
11
 
@@ -71,16 +70,17 @@ class TableQuestionMixin:
71
70
  def format_cell(row_data: Dict, column: str) -> Union[str, ContentAST.Answer]:
72
71
  """Format a cell based on whether it should be an answer or plain data"""
73
72
  value = row_data.get(column, "")
74
-
73
+
75
74
  # 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)
75
+ # Answer extends ContentAST.Leaf, so it can be used directly
76
+ if column in answer_columns and isinstance(value, ContentAST.Answer):
77
+ return value
78
78
  # If this column should contain answers but we have the answer key
79
79
  elif column in answer_columns and isinstance(value, str) and hasattr(self, 'answers'):
80
80
  answer_obj = self.answers.get(value)
81
81
  if answer_obj:
82
- return ContentAST.Answer(answer_obj)
83
-
82
+ return answer_obj
83
+
84
84
  # Otherwise return as plain data
85
85
  return str(value)
86
86
 
@@ -119,9 +119,9 @@ class TableQuestionMixin:
119
119
  # Build data with parameters plus answer row
120
120
  data = [[key, str(value)] for key, value in parameter_info.items()]
121
121
 
122
- # Add answer row
122
+ # Add answer row - Answer extends ContentAST.Leaf so it can be used directly
123
123
  if hasattr(self, 'answers') and answer_key in self.answers:
124
- data.append([answer_label, ContentAST.Answer(self.answers[answer_key])])
124
+ data.append([answer_label, self.answers[answer_key]])
125
125
  else:
126
126
  data.append([answer_label, f"[{answer_key}]"]) # Fallback
127
127
 
@@ -151,14 +151,15 @@ class TableQuestionMixin:
151
151
 
152
152
  def process_cell_value(value: Any) -> Union[str, ContentAST.Answer]:
153
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)
154
+ # If it's already an Answer object, use it directly
155
+ # Answer extends ContentAST.Leaf so it can be used in the AST
156
+ if isinstance(value, ContentAST.Answer):
157
+ return value
157
158
  # If it's a string that looks like an answer key, try to resolve it
158
159
  elif isinstance(value, str) and value.startswith("answer__") and hasattr(self, 'answers'):
159
160
  answer_obj = self.answers.get(value)
160
161
  if answer_obj:
161
- return ContentAST.Answer(answer_obj)
162
+ return answer_obj
162
163
  # Otherwise return as-is
163
164
  return str(value)
164
165
 
@@ -367,9 +368,9 @@ class MultiPartQuestionMixin:
367
368
  Example:
368
369
  # For a 3-part question
369
370
  {
370
- 'a': Answer.integer('a', 5),
371
- 'b': Answer.integer('b', 12),
372
- 'c': Answer.integer('c', -3)
371
+ 'a': ContentAST.Answer.integer('a', 5),
372
+ 'b': ContentAST.Answer.integer('b', 12),
373
+ 'c': ContentAST.Answer.integer('c', -3)
373
374
  }
374
375
  """
375
376
  if not self.is_multipart():
@@ -493,37 +494,52 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
493
494
  subparts.append((operand_a_latex, self.get_operator(), operand_b_latex))
494
495
  return subparts
495
496
 
496
- def get_body(self):
497
+ def _get_body(self):
498
+ """Build question body and collect answers."""
497
499
  body = ContentAST.Section()
498
-
500
+ answers = []
501
+
499
502
  body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
500
-
503
+
501
504
  if self.is_multipart():
502
505
  # Use multipart formatting with repeated problem parts
503
506
  subpart_data = self.generate_subquestion_data()
504
507
  repeated_part = self.create_repeated_problem_part(subpart_data)
505
508
  body.add_element(repeated_part)
509
+ # Collect answers from self.answers dict
510
+ answers = list(self.answers.values())
506
511
  else:
507
512
  # Single equation display
508
513
  equation_latex = self.format_single_equation(self.operand_a, self.operand_b)
509
514
  body.add_element(ContentAST.Equation(f"{equation_latex} = ", inline=False))
510
-
515
+
511
516
  # Canvas-only answer fields (hidden from PDF)
512
- self._add_single_question_answers(body)
513
-
517
+ single_answers = self._add_single_question_answers(body)
518
+ if single_answers:
519
+ answers.extend(single_answers)
520
+
521
+ return body, answers
522
+
523
+ def get_body(self):
524
+ """Build question body (backward compatible interface)."""
525
+ body, _ = self._get_body()
514
526
  return body
515
-
527
+
516
528
  def _add_single_question_answers(self, body):
517
- """Add Canvas-only answer fields for single questions. Subclasses can override."""
529
+ """Add Canvas-only answer fields for single questions. Subclasses can override.
530
+
531
+ Returns:
532
+ List of Answer objects that were added to the body.
533
+ """
518
534
  # Default implementation - subclasses should override for specific answer formats
519
- pass
520
-
521
- def get_explanation(self):
535
+ return []
536
+
537
+ def _get_explanation(self):
522
538
  """Default explanation structure. Subclasses should override for specific explanations."""
523
539
  explanation = ContentAST.Section()
524
-
540
+
525
541
  explanation.add_element(ContentAST.Paragraph([self.get_explanation_intro()]))
526
-
542
+
527
543
  if self.is_multipart():
528
544
  # Handle multipart explanations
529
545
  for i, data in enumerate(self.subquestion_data):
@@ -532,7 +548,12 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
532
548
  else:
533
549
  # Single part explanation
534
550
  explanation.add_element(self.create_single_explanation())
535
-
551
+
552
+ return explanation, []
553
+
554
+ def get_explanation(self):
555
+ """Build question explanation (backward compatible interface)."""
556
+ explanation, _ = self._get_explanation()
536
557
  return explanation
537
558
 
538
559
  def get_explanation_intro(self):
@@ -6,7 +6,7 @@ from typing import List, Dict, Any, Tuple
6
6
  import logging
7
7
 
8
8
  from QuizGenerator.contentast import *
9
- from QuizGenerator.question import Question, QuestionRegistry, Answer
9
+ from QuizGenerator.question import Question, QuestionRegistry
10
10
  from QuizGenerator.mixins import TableQuestionMixin
11
11
 
12
12
  log = logging.getLogger(__name__)
@@ -25,8 +25,8 @@ class FromText(Question):
25
25
 
26
26
  return ContentAST.Section([ContentAST.Text(self.text)])
27
27
 
28
- def get_answers(self, *args, **kwargs) -> Tuple[Answer.AnswerKind, List[Dict[str,Any]]]:
29
- return Answer.AnswerKind.ESSAY, []
28
+ def get_answers(self, *args, **kwargs) -> Tuple[ContentAST.Answer.CanvasAnswerKind, List[Dict[str,Any]]]:
29
+ return ContentAST.Answer.CanvasAnswerKind.ESSAY, []
30
30
 
31
31
 
32
32
  @QuestionRegistry.register()
@@ -6,9 +6,9 @@ import enum
6
6
  import itertools
7
7
  from typing import List, Dict, Optional, Tuple, Any
8
8
 
9
- from QuizGenerator.question import QuestionRegistry, Question, Answer
9
+ from QuizGenerator.question import QuestionRegistry, Question
10
10
 
11
- from QuizGenerator.contentast import ContentAST
11
+ from QuizGenerator.contentast import ContentAST, AnswerTypes
12
12
 
13
13
  import logging
14
14
  log = logging.getLogger(__name__)
@@ -155,170 +155,165 @@ class ValidStringsInLanguageQuestion(LanguageQuestion):
155
155
 
156
156
  super().__init__(*args, **kwargs)
157
157
 
158
- if grammar_str_good is not None and grammar_str_bad is not None:
158
+ # Store whether grammars are fixed (provided) or should be randomized
159
+ self.fixed_grammars = grammar_str_good is not None and grammar_str_bad is not None
160
+ if self.fixed_grammars:
159
161
  self.grammar_str_good = grammar_str_good
160
162
  self.grammar_str_bad = grammar_str_bad
161
163
  self.include_spaces = kwargs.get("include_spaces", False)
162
164
  self.MAX_LENGTH = kwargs.get("max_length", 30)
163
- else:
164
- which_grammar = self.rng.choice(range(4))
165
-
166
- if which_grammar == 0:
167
- # todo: make a few different kinds of grammars that could be picked
168
- self.grammar_str_good = """
169
- <expression> ::= <term> | <expression> + <term> | <expression> - <term>
170
- <term> ::= <factor> | <term> * <factor> | <term> / <factor>
171
- <factor> ::= <number>
172
- <number> ::= <digit> | <number> <digit>
173
- <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
174
- """
175
- # Adding in a plus to number
176
- self.grammar_str_bad = """
177
- <expression> ::= <term> | <expression> + <term> | <expression> - <term>
178
- <term> ::= <factor> | <term> * <factor> | <term> / <factor>
179
- <factor> ::= <number>
180
- <number> ::= <digit> + | <digit> <number>
181
- <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
182
- """
183
- self.include_spaces = False
184
- self.MAX_LENGTH = 30
185
- elif which_grammar == 1:
186
- self.grammar_str_good = """
187
- <sentence> ::= <subject> <verb> <object>
188
- <subject> ::= The cat | A dog | The bird | A child | <adjective> <animal>
189
- <animal> ::= cat | dog | bird | child
190
- <adjective> ::= happy | sad | angry | playful
191
- <verb> ::= chases | sees | hates | loves
192
- <object> ::= the ball | the toy | the tree | <adjective> <object>
193
- """
194
- self.grammar_str_bad = """
195
- <sentence> ::= <subject> <verb> <object>
196
- <subject> ::= The human | The dog | A bird | Some child | A <adjective> <animal>
197
- <animal> ::= cat | dog | bird | child
198
- <adjective> ::= happy | sad | angry | playful
199
- <verb> ::= chases | sees | hates | loves
200
- <object> ::= the ball | the toy | the tree | <adjective> <object>
201
- """
202
- self.include_spaces = True
203
- self.MAX_LENGTH = 100
204
- elif which_grammar == 2:
205
- self.grammar_str_good = """
206
- <poem> ::= <line> | <line> <poem>
207
- <line> ::= <subject> <verb> <object> <modifier>
208
- <subject> ::= whispers | shadows | dreams | echoes | <compound-subject>
209
- <compound-subject> ::= <subject> and <subject>
210
- <verb> ::= dance | dissolve | shimmer | collapse | <compound-verb>
211
- <compound-verb> ::= <verb> then <verb>
212
- <object> ::= beneath | between | inside | around | <compound-object>
213
- <compound-object> ::= <object> through <object>
214
- <modifier> ::= silently | violently | mysteriously | endlessly | <recursive-modifier>
215
- <recursive-modifier> ::= <modifier> and <modifier>
216
- """
217
- self.grammar_str_bad = """
218
- <bad-poem> ::= <almost-valid-line> | <bad-poem> <bad-poem>
219
- <almost-valid-line> ::= <tricky-subject> <tricky-verb> <tricky-object> <tricky-modifier>
220
- <tricky-subject> ::= whispers | shadows and and | <duplicate-subject>
221
- <duplicate-subject> ::= whispers whispers
222
- <tricky-verb> ::= dance | <incorrect-verb-placement> | <verb-verb>
223
- <incorrect-verb-placement> ::= dance dance
224
- <verb-verb> ::= dance whispers
225
- <tricky-object> ::= beneath | <object-verb-swap> | <duplicate-object>
226
- <object-verb-swap> ::= dance beneath
227
- <duplicate-object> ::= beneath beneath
228
- <tricky-modifier> ::= silently | <modifier-subject-swap> | <duplicate-modifier>
229
- <modifier-subject-swap> ::= whispers silently
230
- <duplicate-modifier> ::= silently silently
231
- """
232
- self.include_spaces = True
233
- self.MAX_LENGTH = 100
234
- elif which_grammar == 3:
235
- self.grammar_str_good = """
236
- <A> ::= a <B> a |
237
- <B> ::= b <C> b |
238
- <C> ::= c <A> c |
239
- """
240
- self.grammar_str_bad = """
241
- <A> ::= a <B> c
242
- <B> ::= b <C> a |
243
- <C> ::= c <A> b |
244
- """
245
- self.include_spaces = False
246
- self.MAX_LENGTH = 100
247
-
248
- self.grammar_good = BNF.parse_bnf(self.grammar_str_good, self.rng)
249
- self.grammar_bad = BNF.parse_bnf(self.grammar_str_bad, self.rng)
250
-
251
- self.num_answer_options = kwargs.get("num_answer_options", 4)
252
- self.num_answer_blanks = kwargs.get("num_answer_blanks", 4)
253
-
254
- def refresh(self, *args, **kwargs):
255
- super().refresh(*args, **kwargs)
256
-
257
- self.answers = {}
258
-
165
+ self.grammar_good = BNF.parse_bnf(self.grammar_str_good, self.rng)
166
+ self.grammar_bad = BNF.parse_bnf(self.grammar_str_bad, self.rng)
167
+
259
168
  self.num_answer_options = kwargs.get("num_answer_options", 4)
260
169
  self.num_answer_blanks = kwargs.get("num_answer_blanks", 4)
261
-
262
- self.refresh()
170
+
171
+ def _select_random_grammar(self):
172
+ """Select and set a random grammar. Called from refresh() to ensure each PDF gets different grammar."""
173
+ which_grammar = self.rng.choice(range(4))
174
+
175
+ if which_grammar == 0:
176
+ # todo: make a few different kinds of grammars that could be picked
177
+ self.grammar_str_good = """
178
+ <expression> ::= <term> | <expression> + <term> | <expression> - <term>
179
+ <term> ::= <factor> | <term> * <factor> | <term> / <factor>
180
+ <factor> ::= <number>
181
+ <number> ::= <digit> | <number> <digit>
182
+ <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
183
+ """
184
+ # Adding in a plus to number
185
+ self.grammar_str_bad = """
186
+ <expression> ::= <term> | <expression> + <term> | <expression> - <term>
187
+ <term> ::= <factor> | <term> * <factor> | <term> / <factor>
188
+ <factor> ::= <number>
189
+ <number> ::= <digit> + | <digit> <number>
190
+ <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
191
+ """
192
+ self.include_spaces = False
193
+ self.MAX_LENGTH = 30
194
+ elif which_grammar == 1:
195
+ self.grammar_str_good = """
196
+ <sentence> ::= <subject> <verb> <object>
197
+ <subject> ::= The cat | A dog | The bird | A child | <adjective> <animal>
198
+ <animal> ::= cat | dog | bird | child
199
+ <adjective> ::= happy | sad | angry | playful
200
+ <verb> ::= chases | sees | hates | loves
201
+ <object> ::= the ball | the toy | the tree | <adjective> <object>
202
+ """
203
+ self.grammar_str_bad = """
204
+ <sentence> ::= <subject> <verb> <object>
205
+ <subject> ::= The human | The dog | A bird | Some child | A <adjective> <animal>
206
+ <animal> ::= cat | dog | bird | child
207
+ <adjective> ::= happy | sad | angry | playful
208
+ <verb> ::= chases | sees | hates | loves
209
+ <object> ::= the ball | the toy | the tree | <adjective> <object>
210
+ """
211
+ self.include_spaces = True
212
+ self.MAX_LENGTH = 100
213
+ elif which_grammar == 2:
214
+ self.grammar_str_good = """
215
+ <poem> ::= <line> | <line> <poem>
216
+ <line> ::= <subject> <verb> <object> <modifier>
217
+ <subject> ::= whispers | shadows | dreams | echoes | <compound-subject>
218
+ <compound-subject> ::= <subject> and <subject>
219
+ <verb> ::= dance | dissolve | shimmer | collapse | <compound-verb>
220
+ <compound-verb> ::= <verb> then <verb>
221
+ <object> ::= beneath | between | inside | around | <compound-object>
222
+ <compound-object> ::= <object> through <object>
223
+ <modifier> ::= silently | violently | mysteriously | endlessly | <recursive-modifier>
224
+ <recursive-modifier> ::= <modifier> and <modifier>
225
+ """
226
+ self.grammar_str_bad = """
227
+ <bad-poem> ::= <almost-valid-line> | <bad-poem> <bad-poem>
228
+ <almost-valid-line> ::= <tricky-subject> <tricky-verb> <tricky-object> <tricky-modifier>
229
+ <tricky-subject> ::= whispers | shadows and and | <duplicate-subject>
230
+ <duplicate-subject> ::= whispers whispers
231
+ <tricky-verb> ::= dance | <incorrect-verb-placement> | <verb-verb>
232
+ <incorrect-verb-placement> ::= dance dance
233
+ <verb-verb> ::= dance whispers
234
+ <tricky-object> ::= beneath | <object-verb-swap> | <duplicate-object>
235
+ <object-verb-swap> ::= dance beneath
236
+ <duplicate-object> ::= beneath beneath
237
+ <tricky-modifier> ::= silently | <modifier-subject-swap> | <duplicate-modifier>
238
+ <modifier-subject-swap> ::= whispers silently
239
+ <duplicate-modifier> ::= silently silently
240
+ """
241
+ self.include_spaces = True
242
+ self.MAX_LENGTH = 100
243
+ elif which_grammar == 3:
244
+ self.grammar_str_good = """
245
+ <A> ::= a <B> a |
246
+ <B> ::= b <C> b |
247
+ <C> ::= c <A> c |
248
+ """
249
+ self.grammar_str_bad = """
250
+ <A> ::= a <B> c
251
+ <B> ::= b <C> a |
252
+ <C> ::= c <A> b |
253
+ """
254
+ self.include_spaces = False
255
+ self.MAX_LENGTH = 100
256
+
257
+ self.grammar_good = BNF.parse_bnf(self.grammar_str_good, self.rng)
258
+ self.grammar_bad = BNF.parse_bnf(self.grammar_str_bad, self.rng)
263
259
 
264
260
  def refresh(self, *args, **kwargs):
265
261
  super().refresh(*args, **kwargs)
266
-
262
+
263
+ # Re-select random grammar for each refresh if not using fixed grammars
264
+ if not self.fixed_grammars:
265
+ self._select_random_grammar()
266
+
267
267
  self.answers = {}
268
-
269
- self.answers.update(
270
- {
271
- "answer_good" : Answer(
272
- f"answer_good",
273
- self.grammar_good.generate(self.include_spaces),
274
- Answer.AnswerKind.MULTIPLE_ANSWER,
275
- correct=True
276
- )
277
- }
268
+
269
+ # Create answers with proper ContentAST.Answer signature
270
+ # value is the generated string, correct indicates if it's a valid answer
271
+ good_string = self.grammar_good.generate(self.include_spaces)
272
+ self.answers["answer_good"] = ContentAST.Answer(
273
+ value=good_string,
274
+ kind=ContentAST.Answer.CanvasAnswerKind.MULTIPLE_ANSWER,
275
+ correct=True
278
276
  )
279
-
280
- self.answers.update(
281
- {
282
- "answer_bad":
283
- Answer(
284
- f"answer_bad",
285
- self.grammar_bad.generate(self.include_spaces),
286
- Answer.AnswerKind.MULTIPLE_ANSWER,
287
- correct=False
288
- )
289
- })
290
- self.answers.update({
291
- "answer_bad_early":
292
- Answer(
293
- f"answer_bad_early",
294
- self.grammar_bad.generate(self.include_spaces, early_exit=True),
295
- Answer.AnswerKind.MULTIPLE_ANSWER,
296
- correct=False
297
- )
298
- })
299
-
277
+
278
+ bad_string = self.grammar_bad.generate(self.include_spaces)
279
+ self.answers["answer_bad"] = ContentAST.Answer(
280
+ value=bad_string,
281
+ kind=ContentAST.Answer.CanvasAnswerKind.MULTIPLE_ANSWER,
282
+ correct=False
283
+ )
284
+
285
+ bad_early_string = self.grammar_bad.generate(self.include_spaces, early_exit=True)
286
+ self.answers["answer_bad_early"] = ContentAST.Answer(
287
+ value=bad_early_string,
288
+ kind=ContentAST.Answer.CanvasAnswerKind.MULTIPLE_ANSWER,
289
+ correct=False
290
+ )
291
+
300
292
  answer_text_set = {a.value for a in self.answers.values()}
301
293
  num_tries = 0
302
294
  while len(self.answers) < 10 and num_tries < self.MAX_TRIES:
303
-
295
+
304
296
  correct = self.rng.choice([True, False])
305
297
  if not correct:
306
298
  early_exit = self.rng.choice([True, False])
307
299
  else:
308
300
  early_exit = False
309
- new_answer = Answer(
310
- f"answer_{num_tries}",
311
- (
312
- self.grammar_good
313
- if correct or early_exit
314
- else self.grammar_bad
315
- ).generate(self.include_spaces, early_exit=early_exit),
316
- Answer.AnswerKind.MULTIPLE_ANSWER,
317
- correct= correct and not early_exit
318
- )
319
- if len(new_answer.value) < self.MAX_LENGTH and new_answer.value not in answer_text_set:
320
- self.answers.update({new_answer.key : new_answer})
321
- answer_text_set.add(new_answer.value)
301
+
302
+ generated_string = (
303
+ self.grammar_good
304
+ if correct or early_exit
305
+ else self.grammar_bad
306
+ ).generate(self.include_spaces, early_exit=early_exit)
307
+
308
+ is_correct = correct and not early_exit
309
+
310
+ if len(generated_string) < self.MAX_LENGTH and generated_string not in answer_text_set:
311
+ self.answers[f"answer_{num_tries}"] = ContentAST.Answer(
312
+ value=generated_string,
313
+ kind=ContentAST.Answer.CanvasAnswerKind.MULTIPLE_ANSWER,
314
+ correct=is_correct
315
+ )
316
+ answer_text_set.add(generated_string)
322
317
  num_tries += 1
323
318
 
324
319
  # Generate answers that will be used only for the latex version.
@@ -336,47 +331,55 @@ class ValidStringsInLanguageQuestion(LanguageQuestion):
336
331
  ])()
337
332
  )
338
333
 
339
-
340
- def get_body(self, *args, **kwargs) -> ContentAST.Section:
334
+ def _get_body(self, *args, **kwargs):
335
+ """Build question body and collect answers."""
336
+ answers = list(self.answers.values())
337
+
341
338
  body = ContentAST.Section()
342
-
343
- body.add_elements([
344
- ContentAST.Paragraph([
345
- ContentAST.OnlyHtml([
346
- ContentAST.Text("Given the following grammar, which of the below strings are part of the language?")
347
- ]),
348
- ContentAST.OnlyLatex([
349
- ContentAST.Text(
350
- "Given the following grammar "
351
- "please circle any provided strings that are part of the language (or indicate clearly if there are none), "
352
- "and on each blank line provide generate a new, unique string that is part of the language."
353
- )
339
+
340
+ body.add_element(
341
+ ContentAST.OnlyHtml([
342
+ ContentAST.Paragraph([
343
+ "Given the following grammar, which of the below strings are part of the language?"
354
344
  ])
355
345
  ])
356
- ])
357
-
358
- body.add_element(
359
- ContentAST.Code(self.grammar_good.get_grammar_string())
360
346
  )
361
-
362
- # Add in some answers as latex-only options to be circled
363
347
  body.add_element(
364
348
  ContentAST.OnlyLatex([
365
- ContentAST.Text(f"- `{str(answer)}`")
366
- for answer in self.featured_answers
349
+ ContentAST.Paragraph([
350
+ "Given the following grammar "
351
+ "please circle any provided strings that are part of the language (or indicate clearly if there are none), "
352
+ "and on each blank line provide generate a new, unique string that is part of the language."
353
+ ])
367
354
  ])
368
355
  )
369
-
356
+
357
+ body.add_element(
358
+ ContentAST.Code(self.grammar_good.get_grammar_string())
359
+ )
360
+
361
+ # Add in some answers as latex-only options to be circled
362
+ latex_list = ContentAST.OnlyLatex([])
363
+ for answer in self.featured_answers:
364
+ latex_list.add_element(ContentAST.Paragraph([f"- `{str(answer)}`"]))
365
+ body.add_element(latex_list)
366
+
370
367
  # For Latex-only, ask students to generate some more.
371
368
  body.add_element(
372
369
  ContentAST.OnlyLatex([
373
- ContentAST.AnswerBlock([ContentAST.Answer(Answer.string(f"blank_line_{i}", f"blank_line_{i}"), label=f"blank_line_{i}") for i in range(self.num_answer_blanks)])
370
+ ContentAST.AnswerBlock([AnswerTypes.String("", label="") for i in range(self.num_answer_blanks)])
374
371
  ])
375
372
  )
376
-
373
+
374
+ return body, answers
375
+
376
+ def get_body(self, *args, **kwargs) -> ContentAST.Section:
377
+ """Build question body (backward compatible interface)."""
378
+ body, _ = self._get_body(*args, **kwargs)
377
379
  return body
378
380
 
379
- def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
381
+ def _get_explanation(self, *args, **kwargs):
382
+ """Build question explanation."""
380
383
  explanation = ContentAST.Section()
381
384
  explanation.add_element(
382
385
  ContentAST.Paragraph([
@@ -384,8 +387,13 @@ class ValidStringsInLanguageQuestion(LanguageQuestion):
384
387
  "Unfortunately, there isn't space here to demonstrate the derivation so please work through them on your own!"
385
388
  ])
386
389
  )
390
+ return explanation, []
391
+
392
+ def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
393
+ """Build question explanation (backward compatible interface)."""
394
+ explanation, _ = self._get_explanation(*args, **kwargs)
387
395
  return explanation
388
396
 
389
- def get_answers(self, *args, **kwargs) -> Tuple[Answer.AnswerKind, List[Dict[str,Any]]]:
397
+ def get_answers(self, *args, **kwargs) -> Tuple[ContentAST.Answer.CanvasAnswerKind, List[Dict[str,Any]]]:
390
398
 
391
- return Answer.AnswerKind.MULTIPLE_ANSWER, list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))
399
+ return ContentAST.Answer.CanvasAnswerKind.MULTIPLE_ANSWER, list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))