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.
- QuizGenerator/contentast.py +809 -117
- QuizGenerator/generate.py +219 -11
- QuizGenerator/misc.py +0 -556
- QuizGenerator/mixins.py +50 -29
- QuizGenerator/premade_questions/basic.py +3 -3
- QuizGenerator/premade_questions/cst334/languages.py +183 -175
- QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
- QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
- QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
- QuizGenerator/premade_questions/cst334/process.py +558 -79
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
- QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
- QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
- QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
- QuizGenerator/premade_questions/cst463/models/text.py +29 -15
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
- QuizGenerator/question.py +114 -20
- QuizGenerator/quiz.py +81 -24
- QuizGenerator/regenerate.py +98 -29
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
- QuizGenerator/README.md +0 -5
- QuizGenerator/logging.yaml +0 -55
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
77
|
-
|
|
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
|
|
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,
|
|
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,
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
def
|
|
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
|
|
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.
|
|
29
|
-
return Answer.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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.
|
|
344
|
-
ContentAST.
|
|
345
|
-
ContentAST.
|
|
346
|
-
|
|
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.
|
|
366
|
-
|
|
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([
|
|
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
|
|
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.
|
|
397
|
+
def get_answers(self, *args, **kwargs) -> Tuple[ContentAST.Answer.CanvasAnswerKind, List[Dict[str,Any]]]:
|
|
390
398
|
|
|
391
|
-
return Answer.
|
|
399
|
+
return ContentAST.Answer.CanvasAnswerKind.MULTIPLE_ANSWER, list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))
|