Skip to content
This repository was archived by the owner on Jun 7, 2023. It is now read-only.

Commit 5b85683

Browse files
committed
Add: Dynamic problems for fitb questions.
1 parent 72ecdb8 commit 5b85683

File tree

1 file changed

+66
-45
lines changed

1 file changed

+66
-45
lines changed

runestone/fitb/fitb.py

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ class FillInTheBlank(RunestoneIdDirective):
141141
option_spec = RunestoneIdDirective.option_spec.copy()
142142
option_spec.update(
143143
{
144+
"dynamic": directives.unchanged,
144145
"casei": directives.flag, # case insensitive matching
145146
}
146147
)
@@ -180,8 +181,17 @@ def run(self):
180181

181182
self.updateContent()
182183

183-
self.state.nested_parse(self.content, self.content_offset, fitbNode)
184+
# Process dynamic problem content.
184185
env = self.state.document.settings.env
186+
dynamic = self.options.get("dynamic")
187+
if dynamic:
188+
# Make sure we're server-side.
189+
if not env.config.runestone_server_side_grading:
190+
raise self.error("Dynamic problems require server-side grading.")
191+
# Add in a header to set up the RNG.
192+
fitbNode.template_start = "{{{{ random = get_random({})\n exec({}) }}}}\n".format(repr(self.options["divid"]), repr(dynamic)) + fitbNode.template_start
193+
194+
self.state.nested_parse(self.content, self.content_offset, fitbNode)
185195
self.options["divclass"] = env.config.fitb_div_class
186196

187197
# Expected _`structure`, with assigned variable names and transformations made:
@@ -211,17 +221,21 @@ def run(self):
211221
# self.feedbackArray = [
212222
# [ # blankArray
213223
# { # blankFeedbackDict: feedback 1
214-
# "regex" : feedback_field_name # (An answer, as a regex;
215-
# "regexFlags" : "x" # "i" if ``:casei:`` was specified, otherwise "".) OR
216-
# "number" : [min, max] # a range of correct numeric answers.
217-
# "feedback": feedback_field_body (after being rendered as HTML) # Provides feedback for this answer.
224+
# "regex" : feedback_field_name, # (An answer, as a regex;
225+
# "regexFlags" : "x", # "i" if ``:casei:`` was specified, otherwise "".) OR
226+
# "number" : [min, max], # a range of correct numeric answers OR
227+
# "solution_code" : source_code, # (For dynamic problems -- the dynamically-computed answer.
228+
# "dynamic_code" : source_code, # The first blank also contains setup code.)
229+
# "feedback": feedback_field_body, (after being rendered as HTML) # Provides feedback for this answer.
218230
# },
219231
# { # Feedback 2
220232
# Same as above.
221233
# }
222234
# ],
223235
# [ # Blank 2, same as above.
224-
# ]
236+
# ],
237+
# ...,
238+
# [dynamic_source] # For dynamic problems only.
225239
# ]
226240
#
227241
# ...and a transformed node structure:
@@ -263,47 +277,54 @@ def run(self):
263277
feedback_field_name = feedback_field[0]
264278
assert isinstance(feedback_field_name, nodes.field_name)
265279
feedback_field_name_raw = feedback_field_name.rawsource
266-
# See if this is a number, optinonally followed by a tolerance.
267-
try:
268-
# Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``.
269-
tmp = feedback_field_name_raw.split()
270-
str_num = tmp[0]
271-
list_tol = tmp[1:]
272-
num = ast.literal_eval(str_num)
273-
assert isinstance(num, Number)
274-
# If no tolerance is given, use a tolarance of 0.
275-
if len(list_tol) == 0:
276-
tol = 0
277-
else:
278-
assert len(list_tol) == 1
279-
tol = ast.literal_eval(list_tol[0])
280-
assert isinstance(tol, Number)
281-
# We have the number and a tolerance. Save that.
282-
blankFeedbackDict = {"number": [num - tol, num + tol]}
283-
except (SyntaxError, ValueError, AssertionError):
284-
# We can't parse this as a number, so assume it's a regex.
285-
regex = (
286-
# The given regex must match the entire string, from the beginning (which may be preceded by whitespaces) ...
287-
r"^\s*"
288-
+
289-
# ... to the contents (where a single space in the provided pattern is treated as one or more whitespaces in the student's answer) ...
290-
feedback_field_name.rawsource.replace(" ", r"\s+")
291-
# ... to the end (also with optional spaces).
292-
+ r"\s*$"
293-
)
294-
blankFeedbackDict = {
295-
"regex": regex,
296-
"regexFlags": "i" if "casei" in self.options else "",
297-
}
298-
# Test out the regex to make sure it compiles without an error.
280+
# Simply store the solution code for a dynamic problem.
281+
if dynamic:
282+
blankFeedbackDict = {"solution_code": feedback_field_name_raw}
283+
# For the first blank, also include the dynamic source code.
284+
if not blankArray:
285+
blankFeedbackDict["dynamic_code"] = dynamic
286+
else:
287+
# See if this is a number, optionally followed by a tolerance.
299288
try:
300-
re.compile(regex)
301-
except Exception as ex:
302-
raise self.error(
303-
'Error when compiling regex "{}": {}.'.format(
304-
regex, str(ex)
305-
)
289+
# Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``.
290+
tmp = feedback_field_name_raw.split()
291+
str_num = tmp[0]
292+
list_tol = tmp[1:]
293+
num = ast.literal_eval(str_num)
294+
assert isinstance(num, Number)
295+
# If no tolerance is given, use a tolerance of 0.
296+
if len(list_tol) == 0:
297+
tol = 0
298+
else:
299+
assert len(list_tol) == 1
300+
tol = ast.literal_eval(list_tol[0])
301+
assert isinstance(tol, Number)
302+
# We have the number and a tolerance. Save that.
303+
blankFeedbackDict = {"number": [num - tol, num + tol]}
304+
except (SyntaxError, ValueError, AssertionError):
305+
# We can't parse this as a number, so assume it's a regex.
306+
regex = (
307+
# The given regex must match the entire string, from the beginning (which may be preceded by whitespaces) ...
308+
r"^\s*"
309+
+
310+
# ... to the contents (where a single space in the provided pattern is treated as one or more whitespaces in the student's answer) ...
311+
feedback_field_name.rawsource.replace(" ", r"\s+")
312+
# ... to the end (also with optional spaces).
313+
+ r"\s*$"
306314
)
315+
blankFeedbackDict = {
316+
"regex": regex,
317+
"regexFlags": "i" if "casei" in self.options else "",
318+
}
319+
# Test out the regex to make sure it compiles without an error.
320+
try:
321+
re.compile(regex)
322+
except Exception as ex:
323+
raise self.error(
324+
'Error when compiling regex "{}": {}.'.format(
325+
regex, str(ex)
326+
)
327+
)
307328
blankArray.append(blankFeedbackDict)
308329

309330
feedback_field_body = feedback_field[1]

0 commit comments

Comments
 (0)